skip to Main Content
type MyObj struct {
    Field1 string      `json:"field_1"`
    Field2 int64       `json:"field_2"`
    Field3 string      `json:"field_3"`
    ...
    FieldK string      `json:"field_k"`
    FieldN MyInterface `json:"field_n"`
}

I have a model in my code that (except for the irrelevant domain details) looks like this. The idea of the FieldN field is to support two types, say, MyType1 and MyType2. These have the same CommonMethod() but the models are very different so it’s not about having a parent type with more fields.

Quite expectedly, Go is unable to unmarshal JSON into an interface value. I am trying to use a custom UnmarshalJSON() implementation but so far it looks really awkward:

func (m *MyObj) UnmarshalJSON(data []byte) error {
    out := &MyObj{}

    var m map[string]json.RawMessage
    if err := json.Unmarshal(data, &m); err != nil {
        return err
    }

    if err := json.Unmarshal(m["field_1"], &out.Field1); err != nil {
        return err
    }
    delete(m, "field_1")

    if err := json.Unmarshal(m["field_2"], &out.Field2); err != nil {
        return err
    }
    delete(m, "field_2")

    if err := json.Unmarshal(m["field_3"], &out.Field3); err != nil {
        return err
    }
    delete(m, "field_3")

    ... // from 3 to k-1

    if err := json.Unmarshal(m["field_k"], &out.FieldK); err != nil {
        return err
    }
    delete(m, "field_k")

    var mt1 MyType1
    if err := json.Unmarshal(m["field_n"], &mt1); err == nil {
        s.FieldN = &mt1
        return nil
    }

    var mt2 MyType2
    if err := json.Unmarshal(m["field_n"], &mt2); err == nil {
        s.FieldN = &mt2
        return nil
    }

    return nil
}

The idea of this approach is to first unmarshal all "static" values and then deal with the interface type. There are at least 2 problems, however, with it, in my opinion:

  1. In my case, the number of fields might grow in the future and the code will get even more repetitive than it currently is

  2. Even the current version requires checking that the map m has key field_i otherwise I would just get unexpected end of input. This is even more cumbersome.

Is there a more elegant way to do the following:

  • Unmarshal all fields with static types
  • Handle the only special interface-typed value

Thanks!

Important update:

It should be noted that Field1 effectively defines which concrete type should be used for FieldN. This, as was noted in the comments, should simplify the approach considerably but I still struggle a bit with the correct implementation.

2

Answers


  1. This demo is based on @mkopriva’s suggestion (DisallowUnknownFields) but still use the "try one; if failed, try another" procedure.

    package main
    
    import (
        "bytes"
        "encoding/json"
        "fmt"
    )
    
    type MyObj struct {
        Field1 string      `json:"field_1"`
        FieldN MyInterface `json:"field_n"`
    }
    
    type MyInterface interface{}
    
    type MyType1 struct {
        FF1 string `json:"ff1"`
    }
    
    type MyType2 struct {
        FF2 string `json:"ff2"`
    }
    
    func (m *MyObj) UnmarshalJSON(data []byte) error {
        // We can not use MyObj directly. If we do this, the json decoder will
        // call this func, and result in a stack overflow panic. replace
        // "type MyObj1 MyObj" with "type MyObj1 = MyObj" and you will see the error.
        type MyObj1 MyObj
        out := MyObj1{FieldN: &MyType1{}}
    
        dec := json.NewDecoder(bytes.NewReader(data))
        dec.DisallowUnknownFields()
    
        if err := dec.Decode(&out); err == nil {
            *m = MyObj(out)
            return nil
        }
    
        out.FieldN = &MyType2{}
        dec = json.NewDecoder(bytes.NewReader(data))
        dec.DisallowUnknownFields()
        if err := dec.Decode(&out); err == nil {
            *m = MyObj(out)
            return nil
        } else {
            return err
        }
    }
    
    func main() {
        test(`{"field_1":"field1","field_n":{"ff1":"abc"}}`)
        test(`{"field_1":"field1","field_n":{"ff2":"abc"}}`)
    }
    
    func test(input string) {
        var obj MyObj
    
        if err := json.Unmarshal([]byte(input), &obj); err != nil {
            fmt.Println(err)
        } else {
            fmt.Printf("%#v, %#vn", obj, obj.FieldN)
        }
    }
    

    The output:

    main.MyObj{Field1:"field1", FieldN:(*main.MyType1)(0xc00009e270)}, &main.MyType1{FF1:"abc"}
    main.MyObj{Field1:"field1", FieldN:(*main.MyType2)(0xc00009e3a0)}, &main.MyType2{FF2:"abc"}
    
    Login or Signup to reply.
  2. Use json.RawMessage to capture the varying part of the object. Decode the raw message using type determined in application logic.

    func (m *MyObj) UnmarshalJSON(data []byte) error {
    
        // Declare new type with same fields as MyObj, but
        // but no methods. This type is used to avoid
        // recursion when unmarshaling a value of type 
        // Y declared below.
        type X MyObj
    
        // Declare a type to capture field_n as a raw message
        // and all other fields as normal.  The FieldN in 
        // MyObj is shadowed by the FieldN here.
        type Y struct {
            *X
            FieldN json.RawMessage `json:"field_n"`
        }
    
        // Unmarshal field_n to the raw message and all other fields
        // to m.
        y := Y{X: (*X)(m)}
        err := json.Unmarshal(data, &y)
        if err != nil {
            return err
        }
    
        // We now have field_n as a json.RawMessage in y.FieldN.
        // We can use whatever logic we want to determine the
        // concrete type, create a value of that type, and unmarshal
        // to that value.
        //
        // Here, I assume that field_1 specifies the concrete type.
        switch m.Field1 {
        case "type1":
            m.FieldN = &MyType1{}
        case "type2":
            m.FieldN = &MyType2{}
        default:
            return errors.New("unknown field 1")
        }
    
        return json.Unmarshal(y.FieldN, m.FieldN)
    
    }
    

    https://go.dev/play/p/hV3Lgn1RkBz

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search