How can I implement UnmarshalJSON in this case and only define special behavior for one interface field?

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

How can I implement UnmarshalJSON in this case and only define special behavior for one interface field?

问题

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

我在代码中有一个模型,除了与领域无关的细节外,看起来像这样。FieldN 字段的想法是支持两种类型,比如 MyType1MyType2。它们都有相同的 CommonMethod(),但模型非常不同,所以不是通过拥有更多字段的父类型来解决问题。

可预见的是,Go 无法将 JSON 反序列化为接口值。我尝试使用自定义的 UnmarshalJSON() 实现,但目前看起来非常笨拙:

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

    ... // 从 3 到 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 {
        out.FieldN = &mt1
        return nil
    }

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

    return nil
}

这种方法的思路是首先反序列化所有“静态”值,然后处理接口类型。然而,我认为它存在至少两个问题:

  1. 在我的情况下,字段的数量可能在将来增加,代码将变得更加重复。

  2. 即使是当前版本,也需要检查映射 m 是否具有键 field_i,否则我将只得到 unexpected end of input。这更加麻烦。

是否有更优雅的方法来实现以下操作:

  • 反序列化所有具有静态类型的字段
  • 处理唯一的特殊接口类型的值

谢谢!

重要更新:

应该注意,Field1 实际上定义了 FieldN 应该使用哪种具体类型。正如评论中指出的那样,这应该大大简化方法,但我仍然在正确实现上有些困难。

英文:
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.

答案1

得分: 5

使用json.RawMessage来捕获对象的可变部分。使用应用程序逻辑确定的类型解码原始消息。

func (m *MyObj) UnmarshalJSON(data []byte) error {

    // 声明一个与MyObj具有相同字段但没有方法的新类型。
    // 当解组下面声明的类型Y的值时,使用此类型避免递归。
    type X MyObj

    // 声明一个类型,将field_n作为原始消息捕获,
    // 并将所有其他字段作为普通字段。
    // MyObj中的FieldN被此处的FieldN遮蔽。
    type Y struct {
        *X
        FieldN json.RawMessage `json:"field_n"`
    }

    // 将field_n解组为原始消息,将所有其他字段解组为m。
    y := Y{X: (*X)(m)}
    err := json.Unmarshal(data, &y)
    if err != nil {
        return err
    }

    // 现在,我们在y.FieldN中有一个json.RawMessage作为field_n。
    // 我们可以使用任何逻辑来确定具体类型,创建该类型的值,并对其进行解组。
    //
    // 这里,我假设field_1指定了具体类型。
    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

英文:

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

答案2

得分: 1

这个演示是基于@mkopriva的建议(DisallowUnknownFields),但仍然使用“尝试一个;如果失败,则尝试另一个”的过程。

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 {
	// 我们不能直接使用MyObj。如果这样做,json解码器将调用此函数,并导致堆栈溢出恐慌。
	// 将“type MyObj1 MyObj”替换为“type MyObj1 = MyObj”,您将看到错误。
	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)
	}
}

输出结果:

main.MyObj{Field1:"field1", FieldN:(*main.MyType1)(0xc00009e270)}, &main.MyType1{FF1:"abc"}
main.MyObj{Field1:"field1", FieldN:(*main.MyType2)(0xc00009e3a0)}, &main.MyType2{FF2:"abc"}
英文:

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

huangapple
  • 本文由 发表于 2023年5月5日 21:26:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/76182708.html
匿名

发表评论

匿名网友

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

确定