5

I have seen plenty of ways to marshal/unmarshal structs that only have unexported fields. But how can I do this with mixed fields?

Given a struct:

type Test struct {
    fieldA string `json:"fieldA"`
    FieldB int    `json:"fieldB"`
    FieldC string `json:"fieldC"`
}

How can I write MarshalJSON/UnmarshalJSON functions so that fieldA is transported along with FieldB and FieldC?

The following compiles, but then overflows the call stack when I run it. My guess is I am recursively marshalling the object, but I am not sure of how else to preserve both exported and unexported fields when encoding.

func (t *Test) MarshalJSON() ([]byte, error) {
    return json.Marshal(struct {
         *Test
         FieldA string `json:"fieldA"`
    }{
         t,
         t.fieldA,
    })
}

func (t *Test) UnmarshalJSON(b []byte) error {
    return json.Unmarshal(b, &t)
}

Is there a way to do this? Or should I re-think my data structures and maybe just export the field anyway?

Note: I'm aware I can do each field manually, but I'd like to avoid that if possible to make updating the code more manageable.

2 Answers 2

16

You can create a specific structure to handle the JSON serialization message: http://play.golang.org/p/d057T7qfVB

type Test struct {
    fieldA string
    FieldB int
    FieldC string
}

type TestJSON struct {
    FieldA string `json:"fieldA"`
    FieldB int    `json:"fieldB"`
    FieldC string `json:"fieldC"`
}

func (t *Test) MarshalJSON() ([]byte, error) {
    return json.Marshal(TestJSON{
        t.fieldA,
        t.FieldB,
        t.FieldC,
    })
}

func (t *Test) UnmarshalJSON(b []byte) error {
    temp := &TestJSON{}

    if err := json.Unmarshal(b, &temp); err != nil {
        return err
    }

    t.fieldA = temp.FieldA
    t.FieldB = temp.FieldB
    t.FieldC = temp.FieldC

    return nil
}
Sign up to request clarification or add additional context in comments.

2 Comments

I didn't want to have to maintain multiple structs but mostly because it would have been an inline struct in the function. This is a touch more elegant and workable for clean code. I like it! Thanks so much!
I really like this solution. It lets me convert time fields on the fly. I like being able to just define what the JSON should be.
7

The other solution is completely functional but it requires updates in many places any time a field changes. This solution is easier to maintain.

By using a type alias and struct embedding, we can create a solution that is more DRY. New fields automatically work as expected, unless they are unexported or require custom formatting. In that case, a minimal amount of work is required: simply list the special field in the *JSON struct and include conversion expressions in MarshalJSON and UnmarshalJSON.

package main

/*

The Go JSON module can't not access unexported fields in a struct. So
how do you work with them?

This demonstrates the solution in http://choly.ca/post/go-json-marshalling/
where we have a 2nd struct that embeds the primary struct but adds
fields that will be used to expose the unexported fields.  We then write
MarshalJSON() and UnmarshalJSON() functions that do the right thing.

This also helps in situations where we have fields that require a custom
format only in JSON.
*/

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

// Cranberry stores data.
//  Visible: This field is exported and JSON displays it as usual.
//  invisible: This field is unexported but we want it to be included in JSON.
//  Custom: This field has a custom output format.  We store it as time.Time
//    but when it appears in JSON, it should be in Unix Epoch format.
type Cranberry struct {
    Visible   int       `json:"visible"`
    invisible int       // No tag here
    Custom    time.Time `json:"-"` // Don't output this field (we'll handle it in CranberryJSON).
}

// CranberryAlias is an alias of Cranberry. We use an alias because aliases
// are stripped of any functions and we need a struct without
// MarshalJSON/UnmarshalJSON defined, otherwise we'd get a recursive defintion.
type CranberryAlias Cranberry

// CranberryJSON represents out we represent Cranberry to the JSON package.
type CranberryJSON struct {
    *CranberryAlias       // All the exported fields.
    Invisible       int   `json:"invisible"`
    CustomUnixEpoch int64 `json:"epoch"`
    // FYI: The json tags "invisble" and "epoch" can be any valid JSON tag.
    // It is all a matter of how we want the JSON to be presented externally.
}

// MarshalJSON marshals a Cranberry. (struct to JSON)
func (u *Cranberry) MarshalJSON() ([]byte, error) {
    return json.Marshal(&CranberryJSON{
        CranberryAlias: (*CranberryAlias)(u),
        // Unexported or custom-formatted fields are listed here:
        Invisible:       u.invisible,
        CustomUnixEpoch: u.Custom.Unix(),
    })
}

// UnmarshalJSON unmarshals a Cranberry. (JSON to struct)
func (u *Cranberry) UnmarshalJSON(data []byte) error {
    temp := &CranberryJSON{
        CranberryAlias: (*CranberryAlias)(u),
    }
    if err := json.Unmarshal(data, &temp); err != nil {
        return err
    }

    // Copy the exported fields:
    *u = (Cranberry)(*(temp).CranberryAlias)
    // Each unexported field must be copied and/or converted individually:
    u.invisible = temp.Invisible
    u.Custom = time.Unix(temp.CustomUnixEpoch, 0) // Convert while copying.

    return nil
}

func main() {
    var out []byte
    var err error

    // Demonstration of marshalling: Marshal s (struct) to out ([]byte)
    fmt.Printf("Struct to JSON:\n")
    s := &Cranberry{Visible: 1, invisible: 2, Custom: time.Unix(1521492409, 0)}
    out, err = json.Marshal(s)
    if err != nil {
        panic(err)
    }
    fmt.Printf("      got=%v\n", string(out))
    fmt.Println(` expected={"visible":1,"invisible":2,"epoch":1521492409}`)

    // Demonstration of how to unmarshal: Unmarshal out ([]byte) to n (struct)
    fmt.Printf("JSON to struct:\n")
    var n = &Cranberry{}
    err = json.Unmarshal(out, n)
    if err != nil {
        panic(err)
    }
    fmt.Printf("      got=%+v\n", n)
    fmt.Println(` expected=&{Visible:1 invisible:2 Custom:2018-03-19 xx:46:49 xxxxx xxx}`)
}

The output looks like this:

$ go run minimal.go 
Struct to JSON:
      got={"visible":1,"invisible":2,"epoch":1521492409}
 expected={"visible":1,"invisible":2,"epoch":1521492409}
JSON to struct:
      got=&{Visible:1 invisible:2 Custom:2018-03-19 16:46:49 -0400 EDT}
 expected=&{Visible:1 invisible:2 Custom:2018-03-19 xx:46:49 xxxxx xxx}

I got this idea from http://choly.ca/post/go-json-marshalling/ who deserves all the credit. Credit also goes to @anderson-nascente who lead me to discover the blog post.

1 Comment

Does this approach also work when embedding a Cranberry in a different type, or use a type alias like type OtherBerry CranBerry and then marshal that?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.