Skip to content

mcwalrus/go-jitjson

Repository files navigation

Go-JitJSON

Go Version Go Report Card codecov GoDoc License: MIT

Go library to provide lazy, or just-in-time (jit) parsing of JSON encodings and Go values.

Motivation

Traditional parsing with json.Marshal or json.Unmarshal processes all data immediately, even if encoding or values are never used. Unnecessary parsing leads to wasted CPU cycles in processing, unnecessary memory allocations, and increased pressure on garbage collection operations. Use jitjson as placeholders to store json encodings or Go values until json parsing is actually needed.

Key Features

  • 🪶 Zero dependencies
  • 🛠️ Support for encoding/json/v2
  • 🔗 Integrates with encoding/json interfaces
  • 💾 Reduce memory when working with large JSON datasets
  • 🚀 Improves performance by avoiding unnecessary parsing of JSON

Consider Use

If you intend to use all of your data, jitjson will not provide any benefit.

Installation

This library requires Go version >=1.18:

$ go get github.com/mcwalrus/go-jitjson

Using json/v2

For Go 1.25+, jitjson provides support for encoding/json/v2.

$ export GOEXPERIMENT=jsonv2
$ go doc github.com/mcwalrus/go-jitjson.JitJSONV2

For more information on json/v2, see the offical blog post.

Quick Start

Marshaling

package main

import (
    "fmt"
    "encoding/json"

    "github.com/mcwalrus/go-jitjson"
)

type Person struct {
    Name string
    Age  int
    City string
}

func main() {
    // jitjson: stores value
    jit := jitjson.New(Person{
        Name: "John",
        Age:  30,
        City: "New York",
    })

    // json.Marshal: parses json encoding
    data, err := json.Marshal(jit)
    if err != nil {
        panic(err)
    }

    fmt.Println(string(data)) // Output: {"age":30,"city":"New York","name":"John"}
}

Unmarshaling

Use the NewFromBytes method to create a JitJSON from a JSON encoded string.

package main

import (
    "fmt"
    "encoding/json"

    "github.com/mcwalrus/go-jitjson"
)

type Person struct {
    Name string
    Age  int
    City string
}

func main() {
    // json encoding
    data := `{
        "Name": "John",
        "Age": 30,
        "City": "New York"
    }`

    // json.Unmarshal: stores json encoding
    var jit = jitjson.NewFromBytes[Person]{}
    err := json.Unmarshal(data, jit)
    if err != nil {
        panic(err)
    }

    // jitjson: parses value only when needed
    value, err := jit.Unmarshal()
    if err != nil {
        panic(err)
    }
    
    fmt.Println(value) // Output: {John 30 New York}
}

Using Slices

Consider when your target value is a slice:

package main

import (
    "fmt"
    "github.com/mcwalrus/go-jitjson"
)

type Person struct {
    Name string
    Age  int
    City string
}

func main() {
    jsonArray := []byte(`[
        {"Name":"John","Age":30,"City":"New York"},
        {"Name":"Jane","Age":25,"City":"Los Angeles"},
        {"Name":"Jim","Age":35,"City":"Chicago"}
    ]`)

    // Unmarshal slice
    var jit []*jitjson.JitJSON[Person]
    err := json.Unmarshal(jsonArray, &jit)
    if err != nil {
        panic(err)
    }

    // Unmarshal only the first person ...
    value, err := jit[0].Unmarshal()
    if err != nil {
        panic(err)
    }

    fmt.Println(value) // Output: {John 30 New York}
}

Using Maps

Consider when your target value is a map:

package main

import (
    "fmt"
    "github.com/mcwalrus/go-jitjson"
)

type Person struct {
    Name string
    Age  int
    City string
}

func main() {
    jsonMap := []byte(`{
        1: {"Name":"John","Age":30,"City":"New York"},
        2: {"Name":"Jane","Age":25,"City":"Los Angeles"},
        3: {"Name":"Jim","Age":35,"City":"Chicago"}
    }`)

    // Unmarshal map
    var jitMap map[int]*jitjson.JitJSON[Person]
    err := json.Unmarshal(jsonMap, &jitMap)
    if err != nil {
        panic(err)
    }

    // Select a person
    jit, ok := jitMap[1]
    if !ok {
        panic("missing person")
    }

    // Unmarshal only person (1) ...
    person, err := jit.Unmarshal()
    if err != nil {
        panic(err)
    }

    fmt.Println(person) // Output: {John 30 New York}
}

Nested Fields

Consider when you have nested fields you would want to avoid parsing:

package main

import (
    "fmt"
    "github.com/mcwalrus/go-jitjson"
)

type Address struct {
    Street string
    City   string
    Zip    string
}

type Person struct {
    Name    string
    Age     int
    Address *jitjson.JitJSON[Address]    
}

func main() {
    jsonData := []byte(`{
        "Name": "John",
        "Age": 30,
        "Address": {
            "Street": "123 Main St",
            "City": "New York",
            "Zip": "10001"
        }
    }`)

    // Unmarshal person
    var person Person
    err := json.Unmarshal(jsonData, &person)
    if err != nil {
        panic(err)
    }

    // Unmarshal person's address just-in-time ...
    address, err := person.Address.Unmarshal()
    if err != nil {
        panic(err)
    }

    fmt.Println(address) // Output: {123 Main St New York 10001}
}

Updating Values

Use the Set \ SetBytes methods to update values of JitJSON. Using these methods will force re-parsing of results:

package main

import (
    "fmt"
    "github.com/mcwalrus/go-jitjson"
)

func main() {
    // New JitJSON:
    jit := jitjson.New(Person{
        Name: "John",
        Age:  30,
        City: "New York",
    })

    // Initial encoding ...
    jsonEncoding, err := jit.Marshal()
    if err != nil {
        panic(err)
    }

    // Update the value:
    jit.Set(Person{
        Name: "Jane",
        Age:  25,
        City: "Los Angeles",
    })

    // Updated encoding ...
    jsonEncoding, err = jit.Marshal()
    if err != nil {
        panic(err)
    }

    fmt.Println(string(jsonEncoding)) // Output: {"age":25,"city":"Los Angeles","name":"Jane"}
}

Parsing Multiple Times

Values can be parsed multiple times, which doesn't come with any performance penalty.

package main

import (
    "fmt"
    "github.com/mcwalrus/go-jitjson"
)

type Person struct {
    Name string
    Age  int
    City string
}

func main() {
    jit := jitjson.New(Person{
        Name: "John",
        Age:  30,   
        City: "New York",
    })

     // Initial Marshal ...
    _, err := jit.Marshal()
    if err != nil {
        panic(err)
    }

    // No new allocations
    for i := 0; i < 10; i++ {
        _, err = jit.Marshal()
        if err != nil {
            panic(err)
        }
    }
◊
    jit = jitjson.NewFromBytes([]byte(`{
        "name": "John",
        "age": 30,
        "city": "New York"
    }`))

    // Initial Unmarshal ...
    _, err = jit.Unmarshal()
    if err != nil {
        panic(err)
    }

    // No new allocations
    for i := 0; i < 10; i++ {
        _, err = jit.Unmarshal()
        if err != nil {
            panic(err)
        }
    }
}

Benchmarks

See BENCHMARK.md for comprehensive benchmark results and performance analysis.

Contributing

Please report any issues or feature requests to the GitHub repository.

About

This module is maintained by Max Collier under an MIT License Agreement.

About

A lazy Go parser implementation for encoding/json

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors