goencoding-json-go

Go MarshalJSON behavior with multiple embedded structs


I'm testing out go json marshaling with embedded structs. However, I see that when I embed time.Time, why does it always override the other embedded structs even though they also provide their own custom marshaling? The code below always print out "0001-01-01T00:00:00Z"

package main

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

type A struct {
}

func (a A) Print() {
    fmt.Println("A")
}

type B struct {
    B int
}

func (a A) MarshalJSON() ([]byte, error) {
    return []byte(`"a"`), nil
}

func (b B) MarshalJSON() ([]byte, error) {
    return []byte(`"b"`), nil
}

func (a B) Print() {
    fmt.Println("A")
}

type C struct {
    A
    B
    time.Time
    C int `json:"C"`
}

func main() {
    fmt.Println("Hello, 世界")
    c := C{}
    decode, err := json.Marshal(c)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(string(decode))
}


Solution

  • If you embed multiple types that have identically named fields or methods, then those fields or methods will not be accessible directly anymore.

    Selectors:

    x.f

    1. For a value x of type T or *T where T is not a pointer or interface type, x.f denotes the field or method at the shallowest depth in T where there is such an f. If there is not exactly one f with shallowest depth, the selector expression is illegal.

    That means that, given the following set of types:

    type S1 struct { F string }
    
    type S2 struct { F string }
    
    type S3 struct {
        S1
        S2
    }
    

    the expression s3.F is illegal:

    var s3 S3
    _ = s3.F // ambiguous selector s3.F
    

    this is because there are more than one F at the shallowest depth. You can try it on playground.

    The same rules apply to methods. From this it follows that your type C does NOT satisfy the json.Marshaler interface because it embeds, at the same depth, more that one type that implements the MarshalJSON() method. You can see that for yourself on playground.


    Then, however, the question still remains as to why the embedded time.Time's custom marshaling is used regardless. This is because time.Time implements not only the json.Marshaler interface but also the encoding.TextMarshaler interface (docs & playground). And the json.Marshal's documentation says the following:

    Marshal traverses the value v recursively. If an encountered value implements the Marshaler interface and is not a nil pointer, Marshal calls its MarshalJSON method to produce JSON. If no MarshalJSON method is present but the value implements encoding.TextMarshaler instead, Marshal calls its MarshalText method and encodes the result as a JSON string.

    You can see here that the behaviour described above holds once you also have A or B implement the encoding.TextMarshaler interface, then the time.Time's MarshalText method will not be used anymore.