Idiomatic way to embed struct with custom MarshalJSON() method

前端 未结 5 581
星月不相逢
星月不相逢 2021-02-04 09:30

Given the following structs:

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

type Employee {
    Person
    JobRole string `json:\"jobRole\"`
}
<
相关标签:
5条回答
  • 2021-02-04 09:57

    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))
    }
    
    0 讨论(0)
  • 2021-02-04 10:08

    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.

    0 讨论(0)
  • 2021-02-04 10:10

    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.

    0 讨论(0)
  • 2021-02-04 10:10

    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,
        })
    }
    
    0 讨论(0)
  • 2021-02-04 10:11

    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

    0 讨论(0)
提交回复
热议问题