Idiomatic way to embed struct with custom MarshalJSON() method
Asked Answered
M

5

20

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?

Michiko answered 20/7, 2016 at 19:59 Comment(2)
What if Person's MarshalJSON returned an array? There's no way to merge that into an object. Composition is hard.Dissect
@AlexGuerra: Quite. It's enough to make me wish MarshalJSON always skipped embedded types, for consistency sake. heh. I suppose an entirely different approach is probably called for in my application.Michiko
C
12

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

Crosshatch answered 20/7, 2016 at 20:47 Comment(2)
That works for my specific example, and is helpful (so +1) but I think it misses the main point of what I was trying to ask. What if Person's MarshalJSON adds new fields, or other opaque data?Michiko
@Flimzy good point. I've updated my answer with another exampleCrosshatch
M
4

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.

Michiko answered 26/6, 2020 at 16:41 Comment(2)
This is very useful, thank you. What about if you have a custom UnmarshalJSON, too? :)Fraught
This is actually by far the best answer I found that ensures that if the embedded struct has a custom MarshalJSON implementation that it's still accessible.Nkvd
G
1

While this produces a different output than what the OP wants, I think it is still useful as a technique to prevent MarshalJSON of embedded structs from breaking the marshaling of structs that contain them.

There is a proposal for encoding/json to recognize an inline option in struct tags. If that ever gets implemented, then I think avoiding embedding structs for cases like in OP's example might be the best bet.


Currently, a reasonable workaround was described in a comment on the Go issue tracker and is the basis for this answer. It consists of defining a new type that will have the same memory layout as the original struct being embedded, but none of the methods:

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

package main

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

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

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

// person has all the fields of Person, but none of the methods.
// It exists to be embedded in other structs.
type person Person

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

type EmployeeGood struct {
    *person
    JobRole string `json:"jobRole"`
}

func main() {
    {
        p := Person{"Bob"}
        e := EmployeeBroken{&p, "Sales"}
        output, _ := json.Marshal(e)
        fmt.Printf("%s\n", string(output))
    }
    {
        p := Person{"Bob"}
        e := EmployeeGood{(*person)(&p), "Sales"}
        output, _ := json.Marshal(e)
        fmt.Printf("%s\n", string(output))
    }
}

Outputs:

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

The OP wants {"name":"BOB","jobRole":"Sales"}. To achieve that, one would need to "inline" the object returned by Person.MarshalJSON into the object produced by Employee.MashalJSON, excluding the fields defined in Person.

Glad answered 22/5, 2020 at 8:56 Comment(2)
this is awesome.Labor
However, this is effectively ignoring the custom MarshalJSON on Person ("bob" is not uppercased into "BOB" anymore), so the intent of having a custom MarshalJSON is lost.Gemina
B
0

I've used this approach on parent structs to keep the embedded struct from overriding marshaling:

func (e Employee) MarshalJSON() ([]byte, error) {
  v := reflect.ValueOf(e)

  result := make(map[string]interface{})

  for i := 0; i < v.NumField(); i++ {
    fieldName := v.Type().Field(i).Name
    result[fieldName] = v.Field(i).Interface()
  }

  return json.Marshal(result)
}

It's handy but nests the embedded structs in the output::

{"JobRole":"Sales","Person":{"name":"Bob"}}

For a tiny struct like the one in the question, @Flimzy's answer is good but can be done more succinctly:

func (e Employee) MarshalJSON() ([]byte, error) {
    return json.Marshal(map[string]interface{}{
        "jobRole": e.JobRole,
        "name":    e.Name,
    })
}
Baroscope answered 19/10, 2020 at 2:42 Comment(1)
What's the point in adding all that to have it nested in the end? If you want it nested you would just add to your struct "Person *Person `json:"person"`Leonor
B
-1

A more generic way to support massive fields in both inner and outer fields.

The side effect is you need to write this for every outer structs.

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

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "strings"
)

func Struct2Json2Map(obj interface{}) (map[string]interface{}, error) {
    data, err := json.Marshal(obj)
    if err != nil {
        return nil, err
    }
    var kvs map[string]interface{}
    err = json.Unmarshal(data, &kvs)
    if err != nil {
        return nil, err
    }
    return kvs, nil
}

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

func (p Person) MarshalJSONHelper() (map[string]interface{}, error) {
    return Struct2Json2Map(struct {
        Name string `json:"name"`
    }{
        Name: strings.ToUpper(p.Name),
    })

}

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

func (e Employee) MarshalJSON() ([]byte, error) {
    personKvs, err := e.Person.MarshalJSONHelper()
    if err != nil {
        return nil, err
    }

    type AliasEmployee Employee
    kvs, err := Struct2Json2Map(struct {
        AliasEmployee
    } {
        AliasEmployee(e),
    })

    for k,v := range personKvs {
        kvs[k] = v
    }
    return json.Marshal(kvs)
}

func main() {
    bob := Employee{
        Person: Person{
            Name: "Bob",
        },
        JobRole: "Sales",
    }

    output, err := json.Marshal(bob)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(output))
}
Bacterin answered 6/3, 2020 at 10:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.