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:
In my case, the number of fields might grow in the future and the code will get even more repetitive than it currently is
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:
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.
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
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, %#v\n", 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"}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With