Cherry pick nested JSON fields into a struct in Golang
How to handle non-trivial JSON parsing into Go structs by using a custom Unmarshaler. Or the standard library.

Well hello there! This was a question my brother asked the wider Discord community we’re in not too long ago, and I remember I’ve done this in the past while working at Typeform some years ago.
Suppose you have a struct like this:
type SomeUsefulThing struct {
Title string
Description string
Content string
Created time.Time
}
And a truly horrendous JSON that looks something like this:
{
"entry": {
"id": "something-here",
"type": "post",
"tags": [
"foo",
"bar",
"baz"
],
"annotations": {
"custom-property": "custom-value",
"other-thing": 52,
"raw-configuration": "this is a raw thing here\ndo not mind it, you should\nprobably just ignore it\n",
"description": "an article about existential crisis"
},
"metadata": {
"author": "john doe",
"author-email": "john@doe.com",
"created": "2025-08-22T05:31:31+00:00",
"title": "to whom the bell tolls",
},
"content": "some long content here with\nmultiple paragraphs, etc...\n\n\n"
}
}
As you can see most of it will be discarded, and the fields that we want to parse are all over the place.
The question is, how do we get that JSON into our struct?
The simple way
Create a struct that matches exactly the JSON, and once that was marshaled, extract the fields into your own struct.
type Metadata struct {
Author string `json:"author"`
AuthorEmail string `json:"author-email"`
Created time.Time `json:"created"`
Title string `json:"title"`
}
type Annotations struct {
CustomProperty string `json:"custom-property"`
OtherThing int `json:"other-thing"`
RawConfiguration string `json:"raw-configuration"`
Description string `json:"description"`
}
type Entry struct {
ID string `json:"id"`
Type string `json:"type"`
Tags []string `json:"tags"`
Annotations Annotations `json:"annotations"`
Metadata Metadata `json:"metadata"`
Content string `json:"content"`
}
type Bridge struct {
Entry Entry `json:"entry"`
}
type SomeUsefulThing struct {
Title string
Description string
Content string
Created time.Time
}
func main() {
var b Bridge
err := json.Unmarshal(data, &b)
if err != nil {
log.Fatalf("json unmarshal: %v", err)
}
t := SomeUsefulThing{
Title: b.Entry.Metadata.Title,
Description: b.Entry.Annotations.Description,
Content: b.Entry.Content,
Created: b.Entry.Metadata.Created,
}
fmt.Printf("%+v\n", t)
}
This works if your JSON is kinda small, but can get out of hand really really quickly. Some APIs return a lot of data, and you probably don’t want to hand roll multiple intermediary structs just to be able to deal with the fields.
Of course you could also just not define the fields you don’t actually need in the intermediary structs. In that case they would look like this:
type Metadata struct {
Created time.Time `json:"created"`
Title string `json:"title"`
}
type Annotations struct {
Description string `json:"description"`
}
type Entry struct {
Annotations Annotations `json:"annotations"`
Metadata Metadata `json:"metadata"`
Content string `json:"content"`
}
type Bridge struct {
Entry Entry `json:"entry"`
}
It’s a lot smaller.
The issue still remains if the JSON is deeply nested. Then there’s the other way:
DIY option: writing a custom unmarshaller
When you call json.Unmarshal
on some data, the Unmarshal
function checks whether the thing you’re trying to shove your data into implements the json.Unmarshaler
interface. If it does, then calling json.Unmarshal
with your struct as target will use the custom method.
In this case, an unmarshaler and its usage would look like this:
type SomeUsefulThing struct {
Title string
Description string
Content string
Created time.Time
}
func (s *SomeUsefulThing) UnmarshalJSON(bytes []byte) error {
if len(bytes) < 2 {
return errors.New("invalid json, too short")
}
if string(bytes) == `""` {
return errors.New("invalid json, empty")
}
var i interface{}
err := json.Unmarshal(bytes, &i)
if err != nil {
return fmt.Errorf("invalid json, could not unmarshal into interface: %w", err)
}
// check that the root is an object and not an array
root, ok := i.(map[string]interface{})
if !ok {
return errors.New("invalid json, root is not an object")
}
// check whether the incoming json has key entry at the root
entry, ok := root["entry"].(map[string]interface{})
if !ok {
return errors.New("root entry does not exist, or is not an object")
}
// check for content, as being the easiest
content, ok := entry["content"].(string)
if !ok {
return errors.New("root content does not exist, or is not a string")
}
// check for annotations
annotations, ok := entry["annotations"].(map[string]interface{})
if !ok {
return errors.New("entry annotations does not exist, or is not an object")
}
description, ok := annotations["description"].(string)
if !ok {
return errors.New("entry description does not exist, or is not a string")
}
// check for metadata
metadata, ok := entry["metadata"].(map[string]interface{})
if !ok {
return errors.New("entry metadata does not exist, or is not an object")
}
title, ok := metadata["title"].(string)
if !ok {
return errors.New("entry metadata title does not exist, or is not a string")
}
created, ok := metadata["created"].(string)
if !ok {
return errors.New("entry metadata created does not exist, or is not a string")
}
createdTime, err := time.Parse(time.RFC3339, created)
if err != nil {
return fmt.Errorf("created time is not RFC3339 format: %w", err)
}
s.Content = content
s.Title = title
s.Description = description
s.Created = createdTime
return nil
}
func main() {
var s SomeUsefulThing
err := json.Unmarshal([]byte(data), &s)
if err != nil {
log.Fatalf("json unmarshal: %v", err)
}
fmt.Printf("%+v\n", s)
}
It works, it’s flexible, it’s also tedious. There’s a LOT of code in the unmarshaler. It might just be easier to create the many additional intermediary structs I wrote you probably don’t want to do.
If the structure of the JSON is fixed, and you just need to flatten the data, you could combine the two approaches: instead of using a Bridge
struct to unmarshal everything first, and then assign the values to your desired struct in the main flow, you could instead hide all of that into the unmarshaler.
The hybrid approach
At this point I would create a separate file to hold everything that’s needed for your custom struct for code organisation purposes, and that would look like this:
package main
import (
"encoding/json"
"errors"
"fmt"
"time"
)
type Metadata struct {
Created time.Time `json:"created"`
Title string `json:"title"`
}
type Annotations struct {
Description string `json:"description"`
}
type Entry struct {
Annotations Annotations `json:"annotations"`
Metadata Metadata `json:"metadata"`
Content string `json:"content"`
}
type Bridge struct {
Entry Entry `json:"entry"`
}
type SomeUsefulThing struct {
Title string
Description string
Content string
Created time.Time
}
func (s *SomeUsefulThing) UnmarshalJSON(bytes []byte) error {
var b Bridge
err := json.Unmarshal(bytes, &b)
if err != nil {
return fmt.Errorf("could not unmarshal into Bridge: %w", err)
}
s.Created = b.Entry.Metadata.Created
s.Title = b.Entry.Metadata.Title
s.Description = b.Entry.Annotations.Description
s.Content = b.Entry.Content
return nil
}
This way in your main.go
file all you need to do is this:
package main
const data = `...` // omitted for brevity, but this is the raw JSON payload here
func main() {
var s SomeUsefulThing
err := json.Unmarshal([]byte(data), &s)
if err != nil {
log.Fatalf("json unmarshal: %v", err)
}
fmt.Printf("%+v\n", s)
}
Short, sweet, concise, and we offloaded a lot of the custom parsing into our own unmarshaler, which also offloaded a lot of the work to the standard library’s code.
The other consideration is to use Decoder instead of just straight unmarshalig JSON, in case you have huge files and would rather stream the data in over reading everything into memory before you do anything else.
I like the third option, because I don’t want to write custom code if I can use the core library to do the work for myself, but I also like to encapsulate functionality so it’s neatly in one place all by itself.
Which one you’ll choose will of course depend on what you need to do with the values. I can imagine having to do transformations between incoming JSON data and struct value will need some sort of type checking and custom code.
Photo by Humberto Arellano on Unsplash