Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Idiomatic way to embed struct with custom MarshalJSON() method

Tags:

json

go

embedding

Given the following structs:

type Person struct {
    Name string `json:"name"`
}

type Employee struct {
    *Person
    JobRole string `json:"jobRole"`
}

I can easily marshal an Employee to JSON as expected:

p := Person{"Bob"}
e := Employee{&p, "Sales"}
output, _ := json.Marshal(e)
fmt.Printf("%s\n", string(output))

Output:

{"name":"Bob","jobRole":"Sales"}

But when the embedded struct has a custom MarshalJSON() method...

func (p *Person) MarshalJSON() ([]byte,error) {
    return json.Marshal(struct{
        Name string `json:"name"`
    }{
        Name: strings.ToUpper(p.Name),
    })
}

it breaks entirely:

p := Person{"Bob"}
e := Employee{&p, "Sales"}
output, _ := json.Marshal(e)
fmt.Printf("%s\n", string(output))

Now results in:

{"name":"BOB"}

(Note the conspicuous lack of jobRole field)

This is easily anticipated... the embedded Person struct implements the MarshalJSON() function, which is being called.

The trouble is, it's not what I want. What I want would be:

{"name":"BOB","jobRole":"Sales"}

That is, encode Employee's fields normally, and defer to Person's MarshalJSON() method to marshal its fields, and hand back some tidy JSON.

Now I could add a MarshalJSON() method to Employee as well, but this requires that I know that the embedded type implements MarshalJSON() as well, and either (a) duplicate its logic, or (b) call Person's MarshalJSON() and somehow manipulate its output to fit where I want it. Either approach seems sloppy, and not very future proof (what if an embedded type I don't control some day adds a custom MarshalJSON() method?)

Are there any alternatives here that I haven't considered?

like image 497
Flimzy Avatar asked Jul 20 '16 19:07

Flimzy


2 Answers

Don't put MarshalJSON on Person since that's being promoted to the outer type. Instead make a type Name string and have Name implement MarshalJSON. Then change Person to

type Person struct {
    Name Name `json:"name"`
}

Example: https://play.golang.org/p/u96T4C6PaY


Update

To solve this more generically you're going to have to implement MarshalJSON on the outer type. Methods on the inner type are promoted to the outer type so you're not going to get around that. You could have the outer type call the inner type's MarshalJSON then unmarshal that into a generic structure like map[string]interface{} and add your own fields. This example does that but it has a side effect of changing the order of the final output fields

https://play.golang.org/p/ut3e21oRdj

like image 103
jcbwlkr Avatar answered Oct 16 '22 20:10

jcbwlkr


Nearly 4 years later, I've come up with an answer that is fundamentally similar to @jcbwlkr's, but does not require the intermediate unmarshal/re-marshal step, by using a little bit of byte-slice manipulation to join two JSON segments.

func (e *Employee) MarshalJSON() ([]byte, error) {
    pJSON, err := e.Person.MarshalJSON()
    if err != nil {
        return nil, err
    }
    eJSON, err := json.Marshal(map[string]interface{}{
        "jobRole": e.JobRole,
    })
    if err != nil {
        return nil, err
    }
    eJSON[0] = ','
    return append(pJSON[:len(pJSON)-1], eJSON...), nil
}

Additional details and discussion of this approach here.

like image 45
Flimzy Avatar answered Oct 16 '22 19:10

Flimzy