Handling large JSONs in Golang can be tedious. The most common approach is to unmarshall the JSON into structs and then access the required fields. But this method quickly becomes cumbersome when dealing with massive JSONs with highly nested fields, especially when only a fraction of the JSON is needed. Libraries like GJSON offers solution for this. However, it still requires defining string paths to navigate through a JSON data, which causes Golang to lose type information and reduces IDE's effectiveness.
A technique I found convenient is to create a wrapper struct around GJSON's gjson.Result
. This allows the struct to
"inherit" GJSON's functionality and be extended. For instance, consider the following (Gemini generated) JSON structure:
{
"PokemonCatalog": {
"PokemonList": [
{
"PokeId": ...,
"Name": ...,
"Type": [...],
"Height": ...,
"Weight": ...,
"RandomEvolves": ...,
"WeirdColor": ...,
"Moves": [
{
"MoveName": ...,
"MoveType": ...,
"Power": ...,
"Accuracy": ...,
"RandomMoveProperty": ...,
"MoveEffects": {
"StatusEffect": ...,
"DamageType": ...,
"RandomEffect": ...,
"SubEffect": {
"SubEffectDetail": ...,
"SubEffectValue": ...
}
}
},
],
"RandomPokemonFact": ...,
"RandomPokemonNoise": ...
}
]
}
}
(It is lightly nested and probably is straightforward to just go through the unmarshall-to-struct route however...)
In order to read the Pokémon in the PokemonList
, the first step is to read the root PokemonCatalog
structure:
package pokemon
import (
"github.com/tidwall/gjson"
"pokemonjson/utils"
)
type PokemonCatalog struct {
gjson.Result
}
func NewPokemonCatalog(content gjson.Result) *PokemonCatalog {
catalog := content.Get("PokemonCatalog")
return &PokemonCatalog{
Result: catalog,
}
}
Now, to read the PokemonList
array, in the above struct, simply create a method that reads the PokemonList
and
converts each item into a Pokemon
struct:
package pokemon
func (p *PokemonCatalog) GetPokemons() []*Pokemon {
pokemonList := p.Get("PokemonList").Array()
return utils.Map(pokemonList, func(pokemon gjson.Result) *Pokemon {
return NewPokemon(pokemon)
})
}
Similar to PokemonCatalog
struct, the Pokemon
struct is also a wrapper around gjson.Result
:
package pokemon
type Pokemon struct {
gjson.Result
}
func NewPokemon(content gjson.Result) *Pokemon {
return &Pokemon{content}
}
It contains methods to get attributes of the individual items in PokemonList
:
package pokemon
// ...
func (p *Pokemon) GetName() string {
return p.Get("Name").String()
}
func (p *Pokemon) GetId() string {
return p.Get("PokeId").String()
}
func (p *Pokemon) GetType() []string {
return utils.Map(p.Get("Type").Array(), func(t gjson.Result) string {
return t.String()
})
}
func (p *Pokemon) GetHeight() float64 {
return p.Get("Height").Float()
}
func (p *Pokemon) GetWeight() float64 {
return p.Get("Weight").Float()
}
func (p *Pokemon) GetMoves() []*Move {
return utils.Map(p.Get("Moves").Array(), func(t gjson.Result) *Move {
return NewMove(t)
})
}
You can even go a step further and utilize the power of GJSON Syntax in the methods.
package pokemon
//...
func (p *Pokemon) GetNativeMoves() []*Move {
pokemonTypes := p.GetType() // Get the pokemon types
result := make([]*Move, 0)
for _, pType := range pokemonTypes {
// Find all the moves with the same type as the pokemon
// The surrounding "#" in #(MoveType==%s)# means it will return all matches
// ...whereas a single "#" i.e. #(MoveType==%s) returns the first match
moves := p.Get(fmt.Sprintf("Moves.#(MoveType==%s)#", pType))
result = append(result, utils.Map(moves.Array(), func(t gjson.Result) *Move {
return NewMove(t)
})...)
}
return result
}
The best part is—it eliminates the need to create structs for unnecessary intermediate JSON objects:
package pokemon
func (p *Move) GetDamageType() string {
// don't need to create MoveEffects struct just to get the DamageType
return p.Get("MoveEffects.DamageType").String()
}
And finally, to query JSON structure:
package main
func main() {
content, err := os.ReadFile("response.json")
if err != nil {
panic(err)
}
response := gjson.ParseBytes(content)
// Read the "PokemonCatalog"
catalog := pokemon.NewPokemonCatalog(response)
// Get the "PokemonList" inside the "PokemonCatalog"
pokemons := catalog.GetPokemons()
for _, p := range pokemons {
// Now access the attributes of each pokemon
fmt.Println(fmt.Sprintf("Name: %v", p.GetName()))
fmt.Println(fmt.Sprintf("Type: %v", p.GetType()))
fmt.Println(fmt.Sprintf("Height: %v", p.GetHeight()))
fmt.Println(fmt.Sprintf("Weight: %v", p.GetWeight()))
for i, move := range p.GetMoves() {
fmt.Println(fmt.Sprintf("Move %v", i+1))
fmt.Println(fmt.Sprintf(" Name: %v", move.GetName()))
fmt.Println(fmt.Sprintf(" Type: %v", move.GetType()))
fmt.Println(fmt.Sprintf(" Accuracy: %v", move.GetAccuracy()))
fmt.Println(fmt.Sprintf(" Damage: %v", move.GetDamageType()))
}
for i, move := range p.GetNativeMoves() {
fmt.Println(fmt.Sprintf("Native Move %v", i+1))
fmt.Println(fmt.Sprintf(" Name: %v", move.GetName()))
fmt.Println(fmt.Sprintf(" Type: %v", move.GetType()))
fmt.Println(fmt.Sprintf(" Accuracy: %v", move.GetAccuracy()))
fmt.Println(fmt.Sprintf(" Damage: %v", move.GetDamageType()))
}
fmt.Println("------")
}
}
The code is available on GitHub.