I wanted to perform some additional steps for initializing a data structure inside my implementation UnmarshalJSON
. Calling json.Unmarshal(b, type)
inside that implementation, naturally, causes a stack overflow.
The JSON decoder is continiously trying to look up, if there is a custom UnmarshalJSON
implementation which then again, calls json.Unmarshal
.
Is there another way to do this? Just call the underlying default implementation without causing this?
An easy and common way to avoid this / protect from it is to create a new type with the type
keyword, and use a type conversion to pass a value of this type (the value may be your original value, type conversion is possible because the new type has the original type as its underlying type).
This works because the type
keyword creates a new type, and the new type will have zero methods (it does not "inherit" the methods of the underlying type).
Does this incur some run-time overhead? No. Quoting from Spec: Conversions:
Specific rules apply to (non-constant) conversions between numeric types or to and from a string type. These conversions may change the representation of
x
and incur a run-time cost. All other conversions only change the type but not the representation ofx
.
Let's see an example. We have a Person
type with a numeric Age
, and we want to make sure the Age
cannot be negative (less than 0
).
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func (p *Person) UnmarshalJSON(data []byte) error {
type person2 Person
if err := json.Unmarshal(data, (*person2)(p)); err != nil {
return err
}
// Post-processing after unmarshaling:
if p.Age < 0 {
p.Age = 0
}
return nil
}
Testing it:
var p *Person
fmt.Println(json.Unmarshal([]byte(`{"name":"Bob","age":10}`), &p))
fmt.Println(p)
fmt.Println(json.Unmarshal([]byte(`{"name":"Bob","age":-1}`), &p))
fmt.Println(p)
Output (try it on the Go Playground):
<nil>
&{Bob 10}
<nil>
&{Bob 0}
Of course the same technique works for custom marshaling (MarshalJSON()
) too:
func (p *Person) MarshalJSON() ([]byte, error) {
// Pre-processing before marshaling:
if p.Age < 0 {
p.Age = 0
}
type person2 Person
return json.Marshal((*person2)(p))
}
Testing it:
p = &Person{"Bob", 10}
fmt.Println(json.NewEncoder(os.Stdout).Encode(p))
p = &Person{"Bob", -1}
fmt.Println(json.NewEncoder(os.Stdout).Encode(p))
Output (on the same Go Playground example):
{"name":"Bob","age":10}
<nil>
{"name":"Bob","age":0}
<nil>
A very similar issue is when you define the String() string
method for custom text representation for the fmt
package, and you want to use the default string representation which you modify. Read more about it here: The difference between t and *t
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With