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.
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.