F566
F566

Reputation: 645

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?

Upvotes: 2

Views: 106

Answers (2)

kozmo
kozmo

Reputation: 4490

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

Upvotes: 0

LeGEC
LeGEC

Reputation: 52081

Your HttpResp[T] type implies that you know in advance the correct type T for the "data" part of your response.

In the example of your question, this means that, before looking at any content for the http response at this location in your code, you somehow know that the response will always contain a string: HttpResp[string].

If your intention is to have code that accepts whatever comes at it when executing that specific json.Unmarshal(...), and figure out how to turn that into a go struct later, then json.RawMessage is one of the standard ways to go -- and perhaps you are not looking to use generics at that specific location yet.

Upvotes: -2

Related Questions