Skip to content

tinywasm/json

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

94 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tinywasm/json

A single, platform-agnostic JSON codec for Go that optimizes WebAssembly binary size by using zero reflection. It relies on fmt.Fielder for struct encoding/decoding, which is typically generated by ormc.

Architecture

  • Zero Reflection: Uses type switches and fmt.Fielder instead of the reflect package.
  • Platform-Agnostic: Identical behavior on all platforms (WASM, Linux, macOS, etc.).
  • TinyGo Compatible: Optimized for minimal binary size and memory usage.
  • Fielder-Only: Only types implementing fmt.Fielder can be directly encoded or decoded.

Usage

Code Generation with ormc

fmt.Fielder is never written by hand. The ormc CLI (from github.com/tinywasm/orm) generates Schema() and Pointers() for every struct in the package.

Two modes, controlled by a doc comment on the struct:

Struct type Doc comment What ormc generates
DB-backed model (none) ModelName(), Schema(), Pointers(), Validate(), ReadOne*, ReadAll*
Transport / response // ormc:formonly Schema(), Pointers() only
go install github.com/tinywasm/orm/cmd/ormc@latest
ormc   # run at the module root; writes/rewrites *_orm.go

Structs

Structs MUST implement fmt.Fielder to be supported. This is handled by generating code with ormc.

package main

import (
    "github.com/tinywasm/fmt"
    "github.com/tinywasm/json"
)

// User implements fmt.Fielder (typically via ormc)
type User struct {
    Name string
}

func (u *User) Schema() []fmt.Field {
    return []fmt.Field{{Name: "name", Type: fmt.FieldText}}
}
func (u *User) Pointers() []any { return []any{&u.Name} }

func main() {
    u := User{Name: "Alice"}

    var out string
    if err := json.Encode(&u, &out); err != nil {
        panic(err)
    }
    // out: {"name":"Alice"}

    var result User
    if err := json.Decode(out, &result); err != nil {
        panic(err)
    }

    // Recommended: Explicit validation (if result implements fmt.Validator or uses fmt.ValidateFields)
    // if err := fmt.ValidateFields('c', &result); err != nil {
    //     panic(err)
    // }
}

API

Encode(data fmt.Fielder, output any) error

Serializes to JSON. If data also implements fmt.FielderSlice, it is encoded as a JSON array [...]; otherwise as an object {...}. JSON keys are taken from field.Name. If field.OmitEmpty is true, the field is skipped when its value is zero.

  • data: fmt.Fielder{...} · fmt.FielderSlice[...]
  • output: *[]byte, *string, or io.Writer.

Decode(input any, data fmt.Fielder) error

Parses JSON into data. If data also implements fmt.FielderSlice, input must be a JSON array [...]; otherwise input must be an object {...}.

  • input: []byte, string, or io.Reader.
  • data: fmt.Fielder → expects {...} · fmt.FielderSlice → expects [...]

Pre-Serialized JSON with fmt.RawJSON

For fields containing pre-serialized JSON (e.g., API responses that nest raw JSON), use fmt.RawJSON:

// ormc:formonly
type APIResponse struct {
    Status string     // regular string
    Data   fmt.RawJSON // pre-serialized JSON — emitted inline, not quoted
}

func main() {
    resp := APIResponse{
        Status: "ok",
        Data:   `{"id":123,"name":"Alice"}`, // already JSON-formatted string
    }

    var out string
    json.Encode(&resp, &out)
    // out: {"status":"ok","data":{"id":123,"name":"Alice"}}
    //                        ↑ no quotes around data — emitted inline
}

Why fmt.RawJSON?

  • Eliminates linter warnings about non-standard json tag options
  • Self-documenting: the type signals that the field contains pre-serialized JSON
  • Zero overhead: type alias with =, no runtime boxing

Supported Types and Limitations

To maintain a minimal footprint and zero reflection, tinywasm/json has specific support and constraints:

Supported Field Types

The encoder and decoder directly support the following fmt.FieldType mappings:

Go Type fmt.FieldType JSON Equivalent
string FieldText string
int, int64, etc. FieldInt number
float64, float32 FieldFloat number
bool FieldBool boolean
[]byte FieldBlob string (escaped)
fmt.RawJSON FieldRaw string (pre-serialized JSON, emitted inline)
[]int FieldIntSlice array of numbers
Fielder FieldStruct object (nested)
[]Fielder FieldStructSlice array of objects

Limitations

  • No Reflection: Generic types like map[string]any, []any, or arbitrary structs NOT implementing fmt.Fielder are NOT supported.
  • Root Object or Array: Encode/Decode accept fmt.Fielder (→ {}) or fmt.FielderSlice (→ []) at the root. Bare strings, numbers, or null as root values are not supported.
  • No Maps: Key-value pairs are only supported via struct fields described in the Schema().
  • Simplified Arrays: Supported slice types include []int and collections of objects (via FieldStructSlice). Other types like []string or []float64 are not yet supported.
  • No Custom Marshaling: Standard interfaces like json.Marshaler or json.Unmarshaler are ignored.
  • Fielder Contract: Structs must return pointers to all fields in the same order as the schema via Pointers().

Benchmarks

tinywasm/json is 77% smaller than encoding/json in WASM (~27 KB vs ~119 KB) and zero-reflect, eliminating reflection overhead and heavy dependencies.

Benchmark tinywasm/json encoding/json
Encode 285 ns/op 276 ns/op
Decode 320 ns/op 1078 ns/op

See full results and analysis in benchmarks/README.md.


Contributing

License

See LICENSE for details.

About

A lightweight JSON wrapper for Go that optimizes WebAssembly binary size. It automatically switches between the standard encoding/json for backends and the browser's native JSON API (via syscall/js) for WASM builds.

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

 
 
 

Contributors