jsongogenericsunmarshalling

How to make a field in a generic struct optional in json unmarshal?


A common http response model using generics:

type HttpResp[T any] struct {
    Code int    `json:"code"`
    Msg  string `json:"msg"`
    Data T      `json:"data"`
}

Sometimes the data does not return, it is null or string or number; in any case, I am not too concerned about its value.

If i use struct{} will get error: json: cannot unmarshal string into Go struct field HttpResp[struct {}].data of type struct {}

resp := HttpResp[struct{}]{}
err := json.Unmarshal([]byte(`{"code":200,"msg":"ok","data": "ok"}`), &resp)

I currently found a clumsy way to solve it.

resp := HttpResp[json.RawMessage]{}

Is there a more elegant way?


Solution

  • if you doesn't know what type of is declared in json (null or string or number), you can use *any to mark Data field as optional and then check resp.Data != nil and field's type to cast:

    func TestName(t *testing.T) {
        resp := HttpResp[*any]{}
        err := json.Unmarshal([]byte(`{"code":200,"msg":"ok","data": "ok"}`), &resp)
        if err != nil {
            t.Fatal(err)
        }
        fmt.Println(resp)
        fmt.Println(resp.Data != nil)
        fmt.Println(*resp.Data)
        fmt.Println((*resp.Data).(string))
    
        fmt.Println(`----`)
    
        resp = HttpResp[*any]{}
        err = json.Unmarshal([]byte(`{"code":200,"msg":"ok","data": 1}`), &resp)
        if err != nil {
            t.Fatal(err)
        }
        fmt.Println(resp)
        fmt.Println(resp.Data != nil)
        fmt.Println(*resp.Data)
        fmt.Println((*resp.Data).(float64))
    
        fmt.Println(`----`)
    
        resp = HttpResp[*any]{}
        err = json.Unmarshal([]byte(`{"code":200,"msg":"ok"}`), &resp)
        if err != nil {
            t.Fatal(err)
        }
        fmt.Println(resp)
        fmt.Println(resp.Data == nil)
    }
    
    {200 ok 0xc000112450}
    true
    ok
    ok
    ----
    {200 ok 0xc0001124a0}
    true
    1
    1
    ----
    {200 ok <nil>}
    true
    

    PLAYGROUND

    āš ļø But, I think , you does not need to use generic in this case.


    Or try to use more "elegant" way to unmarshal struct šŸ‘‰šŸ» create custom implementation of the Unmarshaller with types check after unmarshalling:

    type StringFloatStruct struct {
        Val any
    }
    
    func (b *StringFloatStruct) UnmarshalJSON(data []byte) error {
        switch sdata := strings.TrimSpace(string(data)); {
        case sdata == "null":
            return nil
        default: // for example
            fdata, err := strconv.ParseFloat(sdata, 64)
            if err == nil {
                b.Val = fdata
                return nil
            }
    
            b.Val = strings.Trim(sdata, "\"")
            return nil
        }
    }
    
    func (b *StringFloatStruct) IsNil() bool {
        return b.Val == nil
    }
    
    func (b *StringFloatStruct) ValString() (string, bool) {
        v, ok := b.Val.(string)
        return v, ok
    }
    
    func (b *StringFloatStruct) ValFloat64() (float64, bool) {
        v, ok := b.Val.(float64)
        return v, ok
    }
    
    func TestV2(t *testing.T) {
        resp := HttpResp[StringFloatStruct]{}
        err := json.Unmarshal([]byte(`{"code":200,"msg":"ok","data": "ok"}`), &resp)
        if err != nil {
            t.Fatal(err)
        }
        fmt.Println(resp)
        fmt.Println(resp.Data.IsNil())
        fmt.Println(resp.Data.ValString())
        fmt.Println(resp.Data.ValFloat64())
    
        fmt.Println(`----`)
    
        resp = HttpResp[StringFloatStruct]{}
        err = json.Unmarshal([]byte(`{"code":200,"msg":"ok","data": 1}`), &resp)
        if err != nil {
            t.Fatal(err)
        }
        fmt.Println(resp.Data.IsNil())
        fmt.Println(resp.Data.ValString())
        fmt.Println(resp.Data.ValFloat64())
    
        fmt.Println(`----`)
    
        resp = HttpResp[StringFloatStruct]{}
        err = json.Unmarshal([]byte(`{"code":200,"msg":"ok"}`), &resp)
        if err != nil {
            t.Fatal(err)
        }
        fmt.Println(resp.Data.IsNil())
        fmt.Println(resp.Data.ValString())
        fmt.Println(resp.Data.ValFloat64())
    }
    
    false
    ok true
    0 false
    ----
    false
     false
    1 true
    ----
    true
     false
    0 false
    

    PLAYGROUND