The model package defines a typed contract for serialization and deserialization, designed to be 0-alloc, map-free, and reflection-less.
This contract allows models to cross boundaries (JSON, WebAssembly/JavaScript) efficiently by having the models themselves describe how to write and read their fields.
See also: CODEC_AND_FIELDER.md for how the codec relates to Field/Fielder, and WHY_64BIT_ONLY.md for why int64/float64 are the only numeric types.
A type that knows how to write its own fields to a FieldWriter.
type Encodable interface {
EncodeFields(w FieldWriter)
IsNil() bool
}A type that knows how to read its own fields from a FieldReader.
type Decodable interface {
DecodeFields(r FieldReader)
IsNil() bool
}DecodeFields only populates — it never fails. Structural errors (malformed input) are accumulated by
the reader and returned by the entrypoint (e.g. json.Decode). Missing or wrong-type fields produce
ok=false from the reader and are silently skipped. Semantic validation lives in Validate().
Receives typed field values. Concrete implementations (like JSON or JS-value) reuse their own buffers to achieve zero allocations.
type FieldWriter interface {
String(name, val string)
Int(name string, val int64)
Float(name string, val float64)
Bool(name string, val bool)
Bytes(name string, val []byte)
Null(name string)
Raw(name, val string)
Object(name string, val Encodable)
Array(name string, n int) ArrayWriter
}Used within FieldWriter.Array() to emit array elements.
type ArrayWriter interface {
String(val string)
Int(val int64)
Float(val float64)
Bool(val bool)
Bytes(val []byte)
Object(val Encodable)
Close()
}Delivers typed field values by name. Implementations avoid building internal maps for lookups.
type FieldReader interface {
String(name string) (string, bool)
Int(name string) (int64, bool)
Float(name string) (float64, bool)
Bool(name string) (bool, bool)
Bytes(name string) ([]byte, bool)
Object(name string, into Decodable) bool
Array(name string) (ArrayReader, bool)
Raw(name string) (string, bool)
}Used within FieldReader.Array() to read array elements.
type ArrayReader interface {
Len() int
String(i int) string
Int(i int) int64
Float(i int) float64
Bool(i int) bool
Bytes(i int) []byte
Object(i int, into Decodable) bool
}- Zero Allocations: The coding path (visitor pattern) does not allocate on the Go heap. No
anyboxing, nomapoverhead. - Map-Free: Prohibited in the contract to avoid dragging the hashmap runtime into TinyGo/WASM binaries.
- Reflection-Less: Types are handled via explicit, typed method calls.
- Agnostic:
modelonly defines the interfaces. Concrete codecs are implemented in:tinywasm/json: Canonical JSON encoder/decoder.tinywasm/jsvalue: JavaScript boundary (WASM).
Both Encodable and Decodable require an IsNil() method for portable nil checking:
func IsNil(v any) bool {
if v == nil {
return true
}
if enc, ok := v.(Encodable); ok {
return enc.IsNil()
}
if dec, ok := v.(Decodable); ok {
return dec.IsNil()
}
return false
}This allows codecs to distinguish between a nil interface value and a pointer receiver that is nil (e.g., (*User)(nil)).
Usually generated by ormc:
func (u *User) EncodeFields(w model.FieldWriter) {
w.String("id", u.ID)
w.String("name", u.Name)
w.Int("age", int64(u.Age))
if len(u.Tags) > 0 {
aw := w.Array("tags", len(u.Tags))
for _, tag := range u.Tags {
aw.String(tag)
}
aw.Close()
}
}
func (u *User) DecodeFields(r model.FieldReader) {
if v, ok := r.String("id"); ok { u.ID = v }
if v, ok := r.String("name"); ok { u.Name = v }
if v, ok := r.Int("age"); ok { u.Age = int(v) }
if ar, ok := r.Array("tags"); ok {
u.Tags = make([]string, ar.Len())
for i := 0; i < ar.Len(); i++ {
u.Tags[i] = ar.String(i)
}
}
}
func (u *User) IsNil() bool {
return u == nil
}- Codec vs Field/Fielder for architectural separation and which layer uses what.