Golang中复合结构体的MarshalJSON未按预期工作

Golang中复合结构体的MarshalJSON未按预期工作 我正在开发一个接收和发送JSON的API,这没什么特别的 😊 我首先关注的是,我需要在API中只使用UTC时间和日期,因此接收到的任何日期/时间都需要转换为UTC。

为了使这在已有的结构体上生效,我实现了 MarshalJSON() 方法,效果很好。下面是一个简化的示例,用 Contact 结构体来说明:

package main

import (
	"encoding/json"
	"fmt"
	"time"
)

type Contact struct {
	FirstName string    `json:"first_name"`
	LastName  string    `json:"last_name"`
	CreatedAt time.Time `json:"created_at"`
}

func (c *Contact) MarshalJSON() ([]byte, error) {
	type ContactAlias Contact
	return json.Marshal(&struct {
		*ContactAlias
		CreatedAt time.Time `json:"created_at" sql:"created_at"`
	}{
		ContactAlias: (*ContactAlias)(c),
		CreatedAt:    c.CreatedAt.UTC(),
	})
}

func main() {
	contact := &Contact{FirstName: "John", LastName: "Doe", CreatedAt: time.Now()}
	s, err := json.Marshal(&contact)
	if err != nil {
		println(err.Error())
	}
	fmt.Printf("STEP 1)\n\n%s\n\n\n", s)
}

然后,我需要向我的 Contact 结构体添加另一个字段 Intro

package main

import (
	"encoding/json"
	"fmt"
	"time"
)

type Contact struct {
	FirstName string    `json:"first_name"`
	LastName  string    `json:"last_name"`
	CreatedAt time.Time `json:"created_at"`
}

// STEP-1
func (c *Contact) MarshalJSON() ([]byte, error) {
	type ContactAlias Contact
	return json.Marshal(&struct {
		*ContactAlias
		CreatedAt time.Time `json:"created_at" sql:"created_at"`
	}{
		ContactAlias: (*ContactAlias)(c),
		CreatedAt:    c.CreatedAt.UTC(),
	})
}

// STEP-2
type ContactWithIntro struct {
	Contact
	Intro string `json:"intro"`
}

func main() {
	contact := &Contact{FirstName: "John", LastName: "Doe", CreatedAt: time.Now()}
	s, err := json.Marshal(&contact)
	if err != nil {
		println(err.Error())
	}
	fmt.Printf("STEP 1)\n\n%s\n\n\n", s)

	intro := "Hello World!"
	contactWithIntro := ContactWithIntro{
		Contact: *contact,
		Intro:   intro,
	}

	fmt.Printf("STEP 2-a)\n\n%+v\n\n\n", contactWithIntro)

	s, err = json.Marshal(&contactWithIntro)
	if err != nil {
		println(err.Error())
	}
	fmt.Printf("STEP 2-b)\n\n%s\n", s)
}

如你所见,我无法在 ContactWithInfo 的JSON字符串中得到 intro 字段 😕 经过一些测试,我发现 MarshalJSON 是问题的根源,因为如果我移除它,我可以在最终的JSON中得到 info 字段,但那样时间就不再是UTC了……

问题在于我不明白底层发生了什么。

感谢你的支持。

以下是测试代码片段的链接:

Better Go Playground

output

STEP 1)

{"first_name":"John","last_name":"Doe","created_at":"2023-01-23T20:40:24.23Z"}

STEP 2-a)

{Contact:{FirstName:John LastName:Doe CreatedAt:2023-01-23 21:40:24.23 +0100 UTC+1 m=+0.000900097} Intro:Hello World!}

STEP 2-b)

{"first_name":"John","last_name":"Doe","created_at":"2023-01-23T20:40:24.23Z"}

Edit 24/01/2023 - 不理想的解决方案

一个处理UTC时间的解决方案是创建一个与自定义 MarshalJSON 方法关联的自定义类型,但这意味着在将时间值赋值给结构体字段的任何地方都需要进行类型转换。我更希望避免这种解决方案。

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

type Contact struct {
    FirstName string      `json:"first_name"`
    LastName  string      `json:"last_name"`
    CreatedAt DateTimeUTC `json:"created_at"`
}

type DateTimeUTC time.Time

func (c DateTimeUTC) MarshalJSON() ([]byte, error) {
    return json.Marshal(time.Time(c).UTC().Format(time.RFC3339))
}

func main() {
    t := DateTimeUTC(time.Now())
    contact := &Contact{FirstName: "John", LastName: "Doe", CreatedAt: t}
    s, err := json.Marshal(&contact)
    if err != nil {
        println(err.Error())
    }
    fmt.Printf("%s\n", s)
}

代码片段在此:Better Go Playground


更多关于Golang中复合结构体的MarshalJSON未按预期工作的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

我将 Contact 类型嵌入ContactWithIntro 中,并向其方法集添加了 MarshalJSON 方法,因此来自 Contact 结构体的自定义 MarshalJSON 方法被提升到了外层结构体,从而在序列化 ContactWithIntro 时被调用。

因此,我将不得不使用编辑 24/01/2023部分中提到的变通方法,或者在外层结构体 ContactWithIntro 上定义自定义的 marshalJSON 方法。

更多关于Golang中复合结构体的MarshalJSON未按预期工作的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


问题在于当 Contact 结构体嵌入到 ContactWithIntro 时,其 MarshalJSON 方法会被优先调用,导致外层结构体的字段被忽略。这是因为 json.Marshal 会检查值是否实现了 json.Marshaler 接口,而 Contact 实现了该接口。

解决方案是为 ContactWithIntro 也实现自定义的 MarshalJSON 方法,确保所有字段都被正确处理。以下是修改后的代码:

package main

import (
	"encoding/json"
	"fmt"
	"time"
)

type Contact struct {
	FirstName string    `json:"first_name"`
	LastName  string    `json:"last_name"`
	CreatedAt time.Time `json:"created_at"`
}

func (c *Contact) MarshalJSON() ([]byte, error) {
	type ContactAlias Contact
	return json.Marshal(&struct {
		*ContactAlias
		CreatedAt time.Time `json:"created_at"`
	}{
		ContactAlias: (*ContactAlias)(c),
		CreatedAt:    c.CreatedAt.UTC(),
	})
}

type ContactWithIntro struct {
	Contact
	Intro string `json:"intro"`
}

func (c *ContactWithIntro) MarshalJSON() ([]byte, error) {
	type ContactWithIntroAlias ContactWithIntro
	return json.Marshal(&struct {
		*ContactWithIntroAlias
		CreatedAt time.Time `json:"created_at"`
	}{
		ContactWithIntroAlias: (*ContactWithIntroAlias)(c),
		CreatedAt:            c.CreatedAt.UTC(),
	})
}

func main() {
	contact := &Contact{FirstName: "John", LastName: "Doe", CreatedAt: time.Now()}
	s, err := json.Marshal(&contact)
	if err != nil {
		println(err.Error())
	}
	fmt.Printf("STEP 1)\n\n%s\n\n\n", s)

	intro := "Hello World!"
	contactWithIntro := ContactWithIntro{
		Contact: *contact,
		Intro:   intro,
	}

	fmt.Printf("STEP 2-a)\n\n%+v\n\n\n", contactWithIntro)

	s, err = json.Marshal(&contactWithIntro)
	if err != nil {
		println(err.Error())
	}
	fmt.Printf("STEP 2-b)\n\n%s\n", s)
}

输出结果将包含 intro 字段,且 created_at 为UTC时间:

STEP 1)

{"first_name":"John","last_name":"Doe","created_at":"2023-01-23T20:40:24.23Z"}

STEP 2-a)

{Contact:{FirstName:John LastName:Doe CreatedAt:2023-01-23 21:40:24.23 +0100 UTC+1 m=+0.000900097} Intro:Hello World!}

STEP 2-b)

{"first_name":"John","last_name":"Doe","created_at":"2023-01-23T20:40:24.23Z","intro":"Hello World!"}

这种方法确保了嵌入结构体的自定义序列化逻辑不会干扰外层结构体的字段。

回到顶部