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.
- Zero Reflection: Uses type switches and
fmt.Fielderinstead of thereflectpackage. - 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.Fieldercan be directly encoded or decoded.
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.goStructs 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)
// }
}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, orio.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, orio.Reader. - data:
fmt.Fielder→ expects{...}·fmt.FielderSlice→ expects[...]
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
jsontag options - Self-documenting: the type signals that the field contains pre-serialized JSON
- Zero overhead: type alias with
=, no runtime boxing
To maintain a minimal footprint and zero reflection, tinywasm/json has specific support and constraints:
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 |
- No Reflection: Generic types like
map[string]any,[]any, or arbitrary structs NOT implementingfmt.Fielderare NOT supported. - Root Object or Array:
Encode/Decodeacceptfmt.Fielder(→{}) orfmt.FielderSlice(→[]) at the root. Bare strings, numbers, ornullas 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
[]intand collections of objects (viaFieldStructSlice). Other types like[]stringor[]float64are not yet supported. - No Custom Marshaling: Standard interfaces like
json.Marshalerorjson.Unmarshalerare ignored. - Fielder Contract: Structs must return pointers to all fields in the same order as the schema via
Pointers().
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.
See LICENSE for details.