Unmarshalling nested JSON objects with dates in Golang

Issue

I’m a noob with Golang. I managed to get some things done with lots of effort.
I’m dealing with JSON files containing dates in a nested way.

I came across some workaround to unmarshal dates from JSON data into time.Time but I’m having a hard time dealing with nested ones.

The following code (obtained here in StackOverflow) is easy to understand since creates a user-defined function to parse the time objects first to a string and then to time.Time with time.Parse.

package main

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

const dateFormat = "2006-01-02"

const data = `{
    "name": "Gopher",
    "join_date": "2007-09-20"
}`

type User struct {
    Name     string    `json:"name"`
    JoinDate time.Time `json:"join_date"`
}

func (u *User) UnmarshalJSON(p []byte) error {
    var aux struct {
        Name     string `json:"name"`
        JoinDate string `json:"join_date"`
    }

    err := json.Unmarshal(p, &aux)
    if err != nil {
        return err
    }

    t, err := time.Parse(dateFormat, aux.JoinDate)
    if err != nil {
        return err
    }

    u.Name = aux.Name
    u.JoinDate = t

    return nil
}

func main() {
    var u User
    err := json.Unmarshal([]byte(data), &u)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(u.JoinDate.Format(time.RFC3339))
} 

So far, so good.
Now I would like to extend it in order to handle the nested date fields in the JSON, like the example below:

[{
    "name": "Gopher",
    "join_date": "2007-09-20",
    "cashflow": [
        {"date": "2021-02-25", 
        "amount": 100},
        {"date": "2021-03-25",
        "amount": 105}
    ]
}]

The struct that I would like to get is:

type Record []struct {
    Name     string `json:"name"`
    JoinDate time.Time `json:"join_date"`
    Cashflow []struct {
        Date   time.Time `json:"date"`
        Amount int    `json:"amount"`
    } `json:"cashflow"`
}

Thanks for the help.

Solution

To solve this using the patterns you’ve already got, you can write a separate unmarshalling function for the inner struct. You can do that by hoisting the inner struct to its own named struct, and then writing the function.

type CashflowRec struct {
    Date   time.Time `json:"date"`
    Amount int       `json:"amount"`
}

type Record struct {
    Name     string        `json:"name"`
    JoinDate time.Time     `json:"join_date"`
    Cashflow []CashflowRec `json:"cashflow"`
}

You’ve already shown how to write the unmarshalling function for CashflowRec, it looks almost the same as your User function. The unmarshalling function for Record will make use of that when it calls

func (u *Record) UnmarshalJSON(p []byte) error {
    var aux struct {
        Name     string        `json:"name"`
        JoinDate string        `json:"join_date"`
        Cashflow []CashflowRec `json:"cashflow"`
    }

    err := json.Unmarshal(p, &aux)

Working example: https://go.dev/play/p/1X7BJ4NETM0

aside 1 Something amusing I learned while looking at this: because you’ve provided your own unmarshalling function, you don’t actually need the json tags in your original structs. Those are hints for the unmarshaller that the json package provides. You should probably still leave them in, in case you have to marshal the struct later. Here’s it working without those tags: https://go.dev/play/p/G2VWopO_A3t

aside 2 You might find it simpler not to use time.Time, but instead create a new type of your own, and then give that type its own unmarshaller. This gives you the interesting choice for writing only that one unmarshaller, but whether or not this is a win depends on what else you do with the struct later on. Working example that still uses your nested anonymous structs: https://go.dev/play/p/bJUcaw3_r41

type dateType time.Time

type Record struct {
    Name     string   `json:"name"`
    JoinDate dateType `json:"join_date"`
    Cashflow []struct {
        Date   dateType `json:"date"`
        Amount int      `json:"amount"`
    } `json:"cashflow"`
}

func (c *dateType) UnmarshalJSON(p []byte) error {
    var s string
    if err := json.Unmarshal(p, &s); err != nil {
        return err
    }
   t, err := time.Parse(dateFormat, s)
     if err != nil {
        return err
    }
    *c = dateType(t)
    return nil
}

Answered By – Mike K

Answer Checked By – Willingham (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.