Home > OS >  How to handle unmarshaling to a custom interface whose type could only be determined after unmarshal
How to handle unmarshaling to a custom interface whose type could only be determined after unmarshal

Time:08-15

I have a json response like this

{
   "foo" : "bar",
   "object" : {
      "type" : "action",
      "data" : "somedata"
   }
}

Here the object could be one of multiple types. I define the types and have them implement a common interface.

type IObject interface {
    GetType() string
}

type Action struct {
    Type    string    `json:"type"`
    Data    string    `json:"data"`
}

func (a Action) GetType() string {
    return "action"
}

type Activity struct {
    Type        string    `json:"type"`
    Duration    int       `json:"duration"`
}

func (a Activity) GetType() string {
    return "activity"
}

And a response struct

type Response struct {
    Foo    string    `json:"foo"`
    Object IObject   `json:"object"`
}

As the type information of a struct that implements IObject is contained within the struct, there is no way to learn in without unmarshaling. I also cannot change the structure of the json response. Currently I am dealing with this problem using a custom unmarshaller:

func UnmarshalObject(m map[string]interface{}, object *IObject) error {
    if m["type"] == "action" {
        b, err := json.Marshal(m)

        if err != nil {
            return err
        }

        action := Action{}

        if err = json.Unmarshal(b, &action); err != nil {
            return err
        }

        *object = action
        return nil
    }

    if m["type"] == "activity" {
        b, err := json.Marshal(m)

        if err != nil {
            return err
        }

        activity := Activity{}

        if err = json.Unmarshal(b, &activity); err != nil {
            return err
        }

        *object = activity
        return nil
    }

    return errors.New("unknown actor type")
}

func (r *Response) UnmarshalJSON(data []byte) error {
    raw := struct {
        Foo       string        `json:"foo"`
        Object    interface{}   `json:"object"`
    }{}

    err := json.Unmarshal(data, &raw)
    if err != nil {
        return err
    }

    r.Foo = raw.Foo


    if err = UnmarshalObject(raw.Object.(map[string]interface{}), &r.Object); err != nil 
    {
        return err
    }

    return nil
}

So what I do is basically

  1. Unmarshall the object into an interface{}
  2. Typecast to map[string]interface{}
  3. Read the "type" value to determine the type
  4. Create a new instance of the determined type
  5. Marshal back to json
  6. Unmarshal again to the new instance of the determined type
  7. Assign the instance to the field

This feels off and I am not comfortable with it. Especially the marshaling/unmarshaling back and forth. Is there a more elegant way to solve this problem?

CodePudding user response:

You can use json.RawMessage.

func (r *Response) UnmarshalJSON(data []byte) error {
    var raw struct {
        Foo    string          `json:"foo"`
        Object json.RawMessage `json:"object"`
    }
    if err := json.Unmarshal(data, &raw); err != nil {
        return err
    }
    r.Foo = raw.Foo

    var obj struct {
        Type string `json:"type"`
    }
    if err := json.Unmarshal(raw.Object, &obj); err != nil {
        return err
    }
    switch obj.Type {
    case "action":
        r.Object = new(Action)
    case "activity":
        r.Object = new(Activity)
    }
    return json.Unmarshal(raw.Object, r.Object)
}

https://go.dev/play/p/6dqiybS4zNp

  • Related