Accessing Dynamic JSON Objects in Golang

When working with JSON data in Go, you often encounter scenarios where the structure of the JSON object is dynamic or unknown at compile time. This can make it challenging to access nested fields within the JSON object. In this article, we'll explore a utility function that allows you to easily retrieve values from dynamic JSON objects.

 

The Problem

Consider a dynamic JSON object where the structure is not fixed, and you need to access nested fields dynamically. For example:

{
    "name": "John",
    "age": 30,
    "address": {
        "city": "New York",
        "zip": "10001"
    },
    "education": {
        "highschool": {
            "name": "Central High",
            "year": 2005
        },
        "university": {
            "name": "State University",
            "year": 2010,
            "degree": "Computer Science"
        }
    },
    "work_experience": [
        {
            "company": "Tech Corp",
            "position": "Software Engineer",
            "years": 5
        },
        {
            "company": "Innovate Ltd",
            "position": "Senior Developer",
            "years": 3
        }
    ],
    "certifications": [
        {
            "name": "AWS Certified Solutions Architect",
            "year": 2018
        },
        {
            "name": "Certified Kubernetes Administrator",
            "year": 2020
        }
    ],
    "hobbies": ["reading", "traveling"]
}

To access the certifications - name field, you would typically need to know the exact structure and write code to navigate through each level. This can become cumbersome when dealing with deeply nested or dynamic structures.

 

The Solution

We can create a utility function in Go that allows us to access nested fields dynamically.

 

Get Key Value or Element of Array

First we need a function that can get either key value or element of array:

func GetField(data interface{}, field interface{}) interface{} {
    switch v := data.(type) {
    case map[string]interface{}:
        return v[field.(string)]
    case []interface{}:
        index, ok := field.(int)
        if !ok {
            return errors.New("field is not an integer")
        }
        if index < 0 || index >= len(v) {
            return errors.New("index out of range")
        }
        return v[index]
    default:
        return errors.New("unsupported type")
    }
}

 

Recursive Function to get the Fields

Second, we need a recursive function to get the fields:

func GetFields(data interface{}, fields ...interface{}) interface{} {
    firstField, restFields := fields[0], fields[1:]
    if len(restFields) == 0 {
        return GetField(data, firstField)
    } else {
        return GetFields(GetField(data, firstField), restFields...)
    }
}

 

Main Functions

Last, is we call the function:
 

func main() {
    // Sample dynamic JSON

    jsonData := ...

    var data map[string]interface{}
    err := json.Unmarshal([]byte(jsonData), &data)
    if err != nil {
        fmt.Println("Error parsing JSON:", err)
        return
    }

    fmt.Println(GetFields(data, "address", "city"))
    fmt.Println(GetFields(data, "education", "highschool", "name"))

    for _, experience := range GetFields(data, "work_experience").([]interface{}) {
        fmt.Println(GetFields(experience, "company"), GetFields(experience, "position"))
    }

    for _, certification := range GetFields(data, "certifications").([]interface{}) {
        fmt.Println(GetFields(certification, "name"), GetFields(certification, "year"))
    }
}
 

Full main.go

Here is sample of full main.go:

package main

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

func GetFields(data interface{}, fields ...interface{}) interface{} {
    firstField, restFields := fields[0], fields[1:]
    if len(restFields) == 0 {
        return GetField(data, firstField)
    } else {
        return GetFields(GetField(data, firstField), restFields...)
    }
}

func GetField(data interface{}, field interface{}) interface{} {
    switch v := data.(type) {
    case map[string]interface{}:
        return v[field.(string)]
    case []interface{}:
        index, ok := field.(int)
        if !ok {
            return errors.New("field is not an integer")
        }
        if index < 0 || index >= len(v) {
            return errors.New("index out of range")
        }
        return v[index]
    default:
        return errors.New("unsupported type")
    }
}

func main() {
    // Sample dynamic JSON
    jsonData := `{
        "name": "John",
        "age": 30,
        "address": {
            "city": "New York",
            "zip": "10001"
        },
        "education": {
            "highschool": {
                "name": "Central High",
                "year": 2005
            },
            "university": {
                "name": "State University",
                "year": 2010,
                "degree": "Computer Science"
            }
        },
        "work_experience": [
            {
                "company": "Tech Corp",
                "position": "Software Engineer",
                "years": 5
            },
            {
                "company": "Innovate Ltd",
                "position": "Senior Developer",
                "years": 3
            }
        ],
        "certifications": [
            {
                "name": "AWS Certified Solutions Architect",
                "year": 2018
            },
            {
                "name": "Certified Kubernetes Administrator",
                "year": 2020
            }
        ],
        "hobbies": ["reading", "traveling"]
    }`

    var data map[string]interface{}
    err := json.Unmarshal([]byte(jsonData), &data)
    if err != nil {
        fmt.Println("Error parsing JSON:", err)
        return
    }

    fmt.Println(GetFields(data, "address", "city"))
    fmt.Println(GetFields(data, "education", "highschool", "name"))
    for _, experience := range GetFields(data, "work_experience").([]interface{}) {
        fmt.Println(GetFields(experience, "company"), GetFields(experience, "position"))
    }
    for _, certification := range GetFields(data, "certifications").([]interface{}) {
        fmt.Println(GetFields(certification, "name"), GetFields(certification, "year"))
    }
}

 

Explanation

  1. GetFields Function: This function takes a JSON object (data) and a variable number of fields (fields). It recursively retrieves the value of the nested fields. If there are no more fields to process, it calls GetField to get the value of the current field.

  2. GetField Function: This function retrieves the value of a single field from the JSON object. It uses a type switch to handle different types of JSON objects. In this example, we handle map[string]interface{} and also []interface{}.

  3. Main Function: In the main function, we provide an example JSON object, unmarshal it into a map, and use the GetFields function to retrieve the value based what the fields parameter inputted.

 

Conclusion

By using the GetFields and GetField functions, you can easily access nested fields in dynamic JSON objects without knowing the exact structure at compile time. This approach is particularly useful when dealing with APIs or data sources where the JSON structure may vary.

 

Bonus 

There is also case, when you work with mongodb (like i do). Then you need to update the function GetField little bit:

package main

import (
    "errors"
    "go.mongodb.org/mongo-driver/bson"
)

func GetField(data interface{}, field interface{}) interface{} {
    switch v := data.(type) {
    case bson.M:
        return v[field.(string)]
    case bson.A:
        index, ok := field.(int)
        if !ok {
            return errors.New("field is not an integer")
        }
        if index < 0 || index >= len(v) {
            return errors.New("index out of range")
        }
        return v[index]
    default:
        return errors.New("unsupported type")
    }
}