Today I’d like to share a very meat & potatoes use of Go to parse a JSON document that contains another, marshaled JSON document. Go’s encoding/json package has some really fantastic features that can help make parsing very easy. You can easily bring most real-world JSON into nice, tidy, structs with Go’s struct tagging and the Marshaler and Unmarshaler interfaces.

But one case is trickier: JSON documents that contain elements of escaped JSON. They look something like this:

{

"id": 12345,

"name": "Test Document",

"payload": "{\"message\":\"hello!\"}"

}

I don’t advise building applications that create documents like this, but sometimes it’s unavoidable and you’d like to be able to parse this document like any vanilla JSON, in just one step. Perhaps you started with the following two types:

type LogEntry struct {

ID int `json:"id"`

Name string `json:"name"`

Payload string `json:"payload"`

} type LogPayload struct {

Message string `json:"message"`

}

Matt Holt’s json-to-go tool can help generate the initial struct from a JSON example. Try it!

The first thing to change is the type of LogEntry.Payload from a string to a LogPayload . That’s important because it’s what you want to end up with anyway, and it’s how the encoding/json package knows how to process that element. Now the problem is that the actual inbound type of the payload element is a JSON string. You’ll need to implement the Unmarshaler interface on the LogPayload type and make it unmarshal into a string , then into a LogPayload .

func (lp *LogPayload) UnmarshalJSON(b []byte) error {

var s string if err := json.Unmarshal(b, &s); err != nil {

return err

} if err := json.Unmarshal([]byte(s), lp); err != nil {

return err

}



return nil

}

Looks great, but unfortunately the second json.Unmarshal call will cause a recursion that eats up the call stack. You’ll have to unmarshal into an intermediary type, and you can do that by defining a new type with an underlying type of LogPayload , like so:

type fauxLogPayload LogPayload

You can tweak the code above to unmarshal into a fauxLogPayload and then cast the result back to LogPayload .

func (lp *LogPayload) UnmarshalJSON(b []byte) error {

var s string if err := json.Unmarshal(b, &s); err != nil {

return err

} var f fauxLogPayload if err := json.Unmarshal([]byte(s), &f); err != nil {

return err

}



*lp = LogPayload(f)



return nil

}

Now the call site to parse the entire document is nice and concise:

func main() { doc := []byte(`{

"id": 12345,

"name": "Test Document",

"payload": "{\"message\":\"test\"}"

}`) var entry LogEntry if err := json.Unmarshal(doc, &entry); err != nil {

fmt.Println("Error!", err)

} fmt.Printf("%v", entry)

}

You can find the code here on the Go Playground.

I hope this example illustrates how easily Go can separate encoding/decoding concerns from your business logic. You can use this method any time you need to convert something encoded into a rudimentary JSON type back into a more complex user-defined type in Go.

Cheers!

Thanks to Redditors BubuX and quiI for suggestions to link to json-to-go and use Go’s string literals for my JSON in main.go!