tinywasm/json
A single, platform-agnostic JSON codec for Go that optimizes WebAssembly binary size by using zero reflection. It relies on model.Encodable and model.Decodable for struct encoding/decoding.
Architecture
- Zero Reflection: Uses type switches and visitor pattern 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.
- 0-Allocation: Hot path is allocation-free (reusing
fmt.Conv buffers and pooling writers/readers).
- Typed Codec: Only types implementing
model.Encodable/model.Decodable can be directly encoded or decoded.
Usage
Structs — define, don't declare
You do not hand-write a struct with EncodeFields/DecodeFields yourself. You
define the model's fields once as a model.Definition, and ormc
generates the struct plus its Schema()/Pointers()/EncodeFields()/
DecodeFields() into a *_orm.go file. Running tinywasm (the framework)
does this for you automatically: it watches every model.go and regenerates
the matching *_orm.go on save — you never run ormc by hand in normal
development, and you never edit the generated file.
model.go (source of truth — this is what you write):
package app
import "github.com/tinywasm/model"
var LogEntryModel = model.Definition{
Name: "log_entry",
Fields: []model.Field{
{Name: "id", Type: model.FieldText},
{Name: "content", Type: model.FieldText},
{Name: "type", Type: model.FieldInt},
},
}
model_orm.go (generated — never edit; regenerated on every model.go save):
// DO NOT EDIT. generated by github.com/tinywasm/ormc
package app
import "github.com/tinywasm/model"
type LogEntry struct {
Id string
Content string
Type int64
}
func (m *LogEntry) Schema() []model.Field { return LogEntryModel.Fields }
func (m *LogEntry) Pointers() []any { return []any{&m.Id, &m.Content, &m.Type} }
func (m *LogEntry) IsNil() bool { return m == nil }
func (m *LogEntry) EncodeFields(w model.FieldWriter) {
w.String("id", m.Id)
w.String("content", m.Content)
w.Int("type", m.Type)
}
func (m *LogEntry) DecodeFields(r model.FieldReader) {
if v, ok := r.String("id"); ok { m.Id = v }
if v, ok := r.String("content"); ok { m.Content = v }
if v, ok := r.Int("type"); ok { m.Type = v }
}
You just call json against the generated type:
import "github.com/tinywasm/json"
entry := LogEntry{Id: "1", Content: "started", Type: 0}
var out string
if err := json.Encode(&entry, &out); err != nil {
panic(err)
}
// out: {"id":"1","content":"started","type":0}
var result LogEntry
if err := json.Decode(out, &result); err != nil {
panic(err)
}
(Real example, mechanically simplified from app/model.go /
app/model_orm.go in this monorepo.)
json.Encode/json.Decode only require model.Encodable
(EncodeFields/IsNil) and model.Decodable (DecodeFields/IsNil) — the
two methods shown above. model.Fielder/Pointers()/Schema() (the rest of
what ormc generates) are a separate contract used only by the SQL/form
layers; json never touches them. This means a small internal type that is
NOT a model.Definition-backed model — never persisted, never shown in a
form — may implement EncodeFields/DecodeFields/IsNil by hand instead of
going through ormc; see json/tests/raw_test.go's rawFielder for a
minimal hand-written example. That is the exception, not the pattern to reach
for by default — reach for model.Definition + ormc first.
API
Encode(data model.Encodable, output any) error
Serializes to JSON. If data is a collection of objects, it is encoded as a JSON array [...]; otherwise as an object {...}.
- data:
model.Encodable → {...} or [...]
- output:
*[]byte, *string, or io.Writer.
Parses JSON into data.
- input:
[]byte, string, or io.Reader.
- data:
model.Decodable → expects {...} or [...]
Benchmarks
tinywasm/json is 83% smaller than encoding/json in WASM (51 KB vs 270 KB, 20 KB vs 118 KB gzipped), zero-reflect, and 0-allocation on the serialization hot path.
| Benchmark |
tinywasm/json |
encoding/json |
Faster |
Δ allocs |
| Encode |
604 ns/op |
799 ns/op |
+24% |
0 |
| Decode |
1503 ns/op |
3098 ns/op |
+51% |
-3 |
See full results and analysis in benchmarks/README.md.
Contributing
License
See LICENSE for details.