Golang:指针为nil/null时自定义UnmarshalJSON/MarshalJSON失效问题
Go指针类型JSON编解码:null/nil时自定义方法不触发的问题及解决
我尝试在指针类型上实现自定义的UnmarshalJSON和MarshalJSON方法,但当JSON数据为null或指针值为nil时,这些方法均不会被触发,示例代码及输出如下:
package main import ( "encoding/json" "fmt" ) type A struct { B *B `json:"b,omitempty"` } type B int // Only for displaying value instead of // pointer address when calling `fmt.Println` func (b *B) String() string { if b == nil { return "nil" } return fmt.Sprintf("%d", *b) } // This function is not triggered when json // data contains null instead of number value func (b *B) UnmarshalJSON(data []byte) error { fmt.Println("UnmarshalJSON on B called") var value int if err := json.Unmarshal(data, &value); err != nil { return err } if value == 7 { *b = B(3) } return nil } // This function is not triggered when `B` // is pointer type and has `nil` value func (b *B) MarshalJSON() ([]byte, error) { fmt.Println("MarshalJSON on B called") if b == nil { return json.Marshal(0) } if *b == 3 { return json.Marshal(7) } return json.Marshal(*b) } func main() { var a A // this won't call `UnmarshalJSON` json.Unmarshal([]byte(`{ "b": null }`), &a) fmt.Printf("a: %+v\n", a) // this won't call `MarshalJSON` b, _ := json.Marshal(a) fmt.Printf("b: %s\n\n", string(b)) // this would call `UnmarshalJSON` json.Unmarshal([]byte(`{ "b": 7 }`), &a) fmt.Printf("a: %+v\n", a) // this would call `MarshalJSON` b, _ = json.Marshal(a) fmt.Printf("b: %s\n\n", string(b)) }
输出:
a: {B:nil} b: {} UnmarshalJSON on B called a: {B:3} MarshalJSON on B called b: {"b":7}
我的问题如下:
- 为何指针类型对应null/nil值时,UnmarshalJSON/MarshalJSON不会被调用?
- 如何在指针为null/nil时也能触发这些方法,而非在A类型上实现编解码方法并修改b属性?
问题1解答
Go标准库encoding/json对指针类型的编解码有特殊处理逻辑:
- Unmarshal阶段:当JSON值为
null时,库会直接将目标指针设为nil,不会调用指针类型的UnmarshalJSON方法——因为此时指针本身是nil,没有可调用方法的实例接收者。 - Marshal阶段:当指针值为
nil时,库会直接将其序列化为null(带omitempty标签则忽略字段),同样不会触发MarshalJSON——nil指针无法作为方法接收者,自然无法调用自定义方法。
简单来说,自定义编解码方法需要一个非nil的接收者实例才能执行,而null/nil场景下接收者不存在,所以方法不会被触发。
问题2解答
要覆盖null/nil场景的编解码逻辑,不能直接在*B类型上实现方法,需要定义一个包装类型,将*B作为包装类型的内部字段,然后在包装类型上实现UnmarshalJSON和MarshalJSON。这样无论JSON是null还是有效值,都会调用包装类型的方法,我们可以在方法内部处理nil情况。
修改后的示例代码:
package main import ( "encoding/json" "fmt" ) type A struct { B BWrapper `json:"b,omitempty"` } type B int // 定义包装类型,内部持有*B指针 type BWrapper struct { ptr *B } // 实现String方法,用于打印展示 func (bw BWrapper) String() string { if bw.ptr == nil { return "nil" } return fmt.Sprintf("%d", *bw.ptr) } // 在包装类型上实现UnmarshalJSON,覆盖null场景 func (bw *BWrapper) UnmarshalJSON(data []byte) error { fmt.Println("UnmarshalJSON on BWrapper called") // 处理JSON null的情况 if string(data) == "null" { // 自定义逻辑:比如给指针设置默认值0 val := B(0) bw.ptr = &val return nil } // 处理正常数值的情况 var value int if err := json.Unmarshal(data, &value); err != nil { return err } if value == 7 { val := B(3) bw.ptr = &val } else { val := B(value) bw.ptr = &val } return nil } // 在包装类型上实现MarshalJSON,覆盖指针nil场景 func (bw BWrapper) MarshalJSON() ([]byte, error) { fmt.Println("MarshalJSON on BWrapper called") if bw.ptr == nil { // 自定义nil时的序列化值,比如返回0 return json.Marshal(0) } if *bw.ptr == 3 { return json.Marshal(7) } return json.Marshal(*bw.ptr) } func main() { var a A // 现在会触发UnmarshalJSON json.Unmarshal([]byte(`{ "b": null }`), &a) fmt.Printf("a: %+v\n", a) // 现在会触发MarshalJSON b, _ := json.Marshal(a) fmt.Printf("b: %s\n\n", string(b)) // 正常数值场景依然触发 json.Unmarshal([]byte(`{ "b": 7 }`), &a) fmt.Printf("a: %+v\n", a) b, _ = json.Marshal(a) fmt.Printf("b: %s\n\n", string(b)) }
输出结果:
UnmarshalJSON on BWrapper called a: {B:0} MarshalJSON on BWrapper called b: {"b":0} UnmarshalJSON on BWrapper called a: {B:3} MarshalJSON on BWrapper called b: {"b":7}
关键说明:
- 包装类型
BWrapper持有*B作为内部字段,它本身是值类型(也可以定义为指针类型,按需调整),其编解码方法可以覆盖所有场景。 - 在
UnmarshalJSON中先判断JSON是否为null,自定义处理逻辑;在MarshalJSON中判断内部指针是否为nil,返回自定义序列化结果。 - 这种方式不需要修改
A类型的编解码逻辑,所有处理都封装在B相关的包装类型中,符合单一职责原则。
内容的提问来源于stack exchange,提问作者mikolaj semeniuk
相关产品推荐
相关产品推荐

