如何处理将数据解组到一个自定义接口中,其类型只能在解组后确定。

huangapple go评论77阅读模式
英文:

How to handle unmarshaling to a custom interface whose type could only be determined after unmarshaling

问题

我有一个类似这样的 JSON 响应:

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

这里的 object 可能是多种类型之一。我定义了这些类型并让它们实现了一个公共接口。

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"
}

还有一个响应结构体:

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

由于实现了 IObject 接口的结构体的类型信息包含在结构体内部,所以没有办法在不进行解组的情况下获得它。我也不能更改 JSON 响应的结构。目前,我正在使用自定义的解组器来处理这个问题:

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
}

所以我做的基本上是:

  1. 将对象解组为 interface{}
  2. 类型转换为 map[string]interface{}
  3. 读取 "type" 值以确定类型
  4. 创建确定类型的新实例
  5. 转换回 JSON
  6. 再次解组为确定类型的新实例
  7. 将实例赋值给字段

这种方式让我感到不舒服,特别是来回进行的编组/解组。有没有更优雅的方法来解决这个问题?

英文:

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?

答案1

得分: 1

你可以使用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

英文:

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

huangapple
  • 本文由 发表于 2022年8月14日 19:02:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/73351086.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定