Documentation
¶
Overview ¶
Package zapv2 is the elegant, generic, idiomatic Go reference implementation of the ZAP wire format.
Where the v1 github.com/luxfi/zap package exposes a C-shaped API (one hand-rolled Wrap/Build/accessor block per schema, runtime offset constants, runtime kind-byte switch), v2 expresses the same wire format through Go 1.23+ generics so that every schema gets:
- One typed View over the buffer.
- One typed Field per offset, with the field's value type enforced at compile time. A Field[SchemaA, uint64] cannot be read from a View[SchemaB]; the compiler rejects it.
- One generic List with iter.Seq range-over-func iteration.
- One generic Pool for reuse.
- One generic Registry for kind-byte dispatch (reflection at boot, none in the hot path).
Wire compatibility ¶
v2 reads and writes the exact same bytes as v1. Both APIs ship side-by-side and may share buffers freely; nothing about the on-wire encoding has changed. v2 is purely an ergonomic upgrade.
Threat model ¶
All wire-level safety properties of v1 are inherited verbatim by v2 (Magic check, version check, bounds-checked offsets, signed-but- non-header-aliasing Object/List pointers, RED-HIGH-1 length clamp, RED-HIGH-2 header-alias rejection). v2 adds compile-time type safety on top — it does not loosen any runtime check.
Hickey-style decomposition ¶
- A schema is a value (something implementing Schema), not a place. It carries a kind byte, a fixed size, and a name. Schemas are qualified by package, not by prefix.
- A view is a value (View) — a typed reference into a buffer. View[S] and View[T] are distinct at compile time.
- A field is a value (Field) — a phantom-typed offset, qualified by the schema it belongs to and the wire type it reads/writes.
- A list is a value (List) — its element type is the schema for that element; iteration is generic via iter.Seq.
One and only one way to express any of these. No braiding of policy with primitive, no inheritance, no duplication.
Index ¶
- Variables
- func Collect[T any](src iter.Seq[T]) []T
- func Count[T any](src iter.Seq[T]) int
- func Filter[T any](src iter.Seq[T], pred func(T) bool) iter.Seq[T]
- func Map[T, U any](src iter.Seq[T], f func(T) U) iter.Seq[U]
- func Read[S Schema, T FieldKind](v View[S], f Field[S, T]) T
- func ReadPayload[S Schema, T FieldKind](payload []byte, f Field[S, T]) T
- func Register[S Schema](r *Registry)
- func RootOff[S Schema](v View[S]) int32
- func Take[T any](src iter.Seq[T], n int) iter.Seq[T]
- func Write[S Schema, T FieldKind](s Setter[S], f Field[S, T], val T)
- func WriteB[S Schema, T FieldKind](bb Builder[S], f Field[S, T], val T)
- func WriteList[S, E Schema](s Setter[S], byteOffset uint32, each func(*ElemSetter[E]))
- type Builder
- type ElemSetter
- type Entry
- type Field
- type FieldKind
- type KindByte
- type List
- type Pool
- type Raw
- type Registry
- type Schema
- type SchemaError
- type Setter
- type View
Constants ¶
This section is empty.
Variables ¶
var DefaultRegistry = NewRegistry()
DefaultRegistry is the package-level registry. Schemas typically register here at init.
Functions ¶
func Collect ¶
Collect drains src into a freshly allocated slice. Useful in tests where the caller wants to assert on the materialized list; not recommended in the hot path because it defeats the zero-copy property of List.
func Count ¶
Count returns the number of elements yielded by src. Drains the sequence — do not call on infinite sequences.
func Filter ¶
Filter returns an iter.Seq yielding only those elements of src for which pred returns true. Constant memory; one allocation per call (the closure), zero per element.
func Map ¶
Map returns an iter.Seq[U] obtained by applying f to each element of src. Constant memory per element.
func Read ¶
Read reads the field f's value from view v.
Compile-time safety: the type parameters S and T are inferred from the (view, field) pair. Passing a Field for a different schema to a View[S] fails to type-check because Field[OtherSchema, T] is not assignable to Field[S, T].
Zero copy, zero allocation. Out-of-range offsets return the zero value of T (matches the audited v1 zap.Object bounds-check behaviour: degrade gracefully to zero rather than panic).
Performance: this function reads bytes directly from the View's pre-sliced payload via unsafe.Pointer, folding to a single load instruction per concrete T after generic instantiation. Verified by [bench_test.go]: matches hand-rolled v1 zap.Object.Uint64 to within compiler-noise.
Wire format: ZAP integers are little-endian. On little-endian hosts (every Lux target — amd64, arm64, aarch64) the load is a direct cast. The pkg builds only on little-endian platforms; a future big- endian port would need a per-type byteswap (see init()).
func ReadPayload ¶
ReadPayload reads a field directly from a payload byte slice (as yielded by List.Payloads). This is the hot-path counterpart to Read — equivalent wire semantics, but the input is a 24-byte slice header (registers-friendly) rather than a 56-byte [View[S]] (stack-spilling).
Use ReadPayload inside a for-range over List.Payloads when iterating large lists with fixed-field reads only. For full View operations (Bytes, sub-objects, lists) use Read with List.All.
Compile-time safety: same as Read — Field[S, T] is phantom-typed against schema S, so the compiler rejects cross-schema misuse even though the payload slice itself is plain []byte. The S type parameter is what ties the field to its declared schema.
func Register ¶
Register registers schema S in r. It is safe to call from multiple goroutines, but the typical pattern is init():
func init() {
zapv2.Register[AdvanceTimeSchema](DefaultRegistry)
}
Register panics if two schemas declare the same kind byte — that would be a wire-protocol bug, not a runtime condition.
func RootOff ¶
RootOff returns the absolute byte offset of the root object within the underlying buffer. Exposed for codegen'd byte-array accessors that need to re-anchor a zap.Object at the root to call v1's out-of-line byte-slice readers (zap.Object.BytesFixedSlice). Application code typically does not need this — use the typed accessors emitted alongside each schema.
func Take ¶
Take returns an iter.Seq yielding at most n elements from src. If src yields fewer than n, all of them are passed through; if more, the iteration stops after n.
Constant memory: Take does not buffer. It counts and short-circuits.
func Write ¶
Write writes value val into field f of setter s.
Compile-time safety: as with Read, passing a Field declared for a different schema fails to type-check.
Zero allocation. The write goes through the underlying v1 zap.ObjectBuilder for the fixed payload — its bounds-check (ensureField) is inherited verbatim. After generic instantiation + inlining, this collapses to a direct little-endian store at the computed offset, matching v1 hand-rolled SetUint64 performance — verified by [bench_test.go].
Implementation note: writing through the ObjectBuilder (rather than a direct unsafe store into the underlying buffer) preserves v1's audited "ensureField" semantics — fields past the currently-written region get the buffer growth + zero-fill that the builder guarantees. A bare unsafe write would bypass that and corrupt the builder's invariant.
func WriteB ¶
WriteB is the Builder-receiver counterpart to Write. Used in the imperative-style build path:
bb := zapv2.NewBuilderFor[Schema]() zapv2.WriteB(bb, Fields.Time, ts) view, buf := bb.Finish()
Identical wire semantics to Write — the difference is only that passing a Builder[S] (by value) instead of a Setter[S] (through a closure) keeps the wrapper on the caller's stack. The underlying v1 *zap.Builder still escapes (its buffer is what goes on the wire), so the imperative form does not eliminate all v2 overhead — it eliminates only the closure-captured pointer allocation.
func WriteList ¶
func WriteList[S, E Schema](s Setter[S], byteOffset uint32, each func(*ElemSetter[E]))
WriteList writes a homogeneous list of element-schema E into the list-pointer field at byteOffset within schema S. The `each` closure is called once with an [*ElemSetter[E]] handle that writes one element per call to ElemSetter.Append; the list pointer in S's fixed payload is patched with the final (relOffset, length) when each returns.
Example (BatchTx.Items, see examples/batch_tx.go):
zapv2.Build[BatchSchema](func(s zapv2.Setter[BatchSchema]) {
zapv2.Write(s, BatchFields.ID, batchID)
zapv2.WriteList[BatchSchema, ItemSchema](s, OffsetBatchItems,
func(items *zapv2.ElemSetter[ItemSchema]) {
for _, it := range source {
items.Append(func(e zapv2.Setter[ItemSchema]) {
zapv2.Write(e, ItemFields.ID, it.id)
zapv2.Write(e, ItemFields.Value, it.val)
})
}
})
})
Stride is taken from E{}.Size(). The list lives in the variable section of the parent message, after the fixed payload — the exact wire layout that v1 zap.Builder.StartList/SetList produces.
Wire encoding: the list-pointer field at byteOffset holds (relOffset, length); length is the number of elements (NOT bytes), matching v1's zap.List.Object indexing convention.
Types ¶
type Builder ¶
type Builder[S Schema] struct { // contains filtered or unexported fields }
Builder is the imperative-style construction API for schema S. Where Build takes a closure (more readable, more allocations because the closure escapes), Builder gives the caller direct control over the build sequence — at the cost of three lines instead of one. Use it on hot paths that match the v1 hand-rolled allocation profile (1 alloc per Finish: the buffer itself).
Usage:
bb := zapv2.NewBuilderFor[AdvanceTimeSchema]() zapv2.WriteB(bb, examples.AdvanceTimeFields.Time, ts) view, buf := bb.Finish()
WriteB is the explicit-receiver counterpart to Write — both take a (Field, value) pair, but WriteB writes through a Builder[S] instead of a Setter[S], allowing escape analysis to keep the builder on the stack.
Builder is NOT safe for concurrent use; each goroutine should allocate its own.
Layout: Builder holds two pointers (b, ob) into the v1 builder state. Both pointers' targets are heap-allocated by v1, but the Builder[S] struct itself stays on the caller's stack because it's passed by value.
func NewBuilderFor ¶
NewBuilderFor returns a fresh Builder[S] sized for S's fixed payload + the ZAP frame header. Variable-length tails (lists, bytes) grow on demand via the underlying zap.Builder.
The discriminator byte at object offset 0 is written automatically; the caller MUST NOT overwrite offset 0.
Returned by value so the wrapping Builder[S] struct can stay on the caller's stack — only the underlying v1 *zap.Builder and []byte buf escape to the heap.
func (Builder[S]) AsSetter ¶
AsSetter returns a Setter[S] for advanced variable-length helpers (WriteList, WriteBytes) that need both the ObjectBuilder and the underlying Builder.
func (Builder[S]) Finish ¶
Finish finalizes the message and returns both the typed View and the underlying buffer (aliasing the same memory). After Finish, the Builder MUST NOT be reused.
Performance: returns a value-typed View[S] holding only slice headers and an int32 offset. No *zap.Message is allocated — the View references buf directly. Combined with NewBuilderFor + WriteB, this collapses to v1's hand-rolled 1-alloc profile (just the buf).
type ElemSetter ¶
type ElemSetter[E Schema] struct { // contains filtered or unexported fields }
ElemSetter writes one element at a time into a list of schema E. Always passed by pointer so WriteList observes the final count after [each] returns.
func (*ElemSetter[E]) Append ¶
func (es *ElemSetter[E]) Append(init func(Setter[E]))
Append writes one element by calling init with a Setter[E] over the element's fixed slot. The slot is zero-filled before init runs.
init MUST write exactly the fields the schema expects; any byte within the slot that init does not touch is left as zero.
type Entry ¶
type Entry struct {
Kind KindByte
Name string
// Wrap validates b and returns (validated data, root offset) if
// the kind byte matches; otherwise returns a SchemaError.
//
// Returns NO *zap.Message — the v1.1 generic API is value-typed
// and constructs its own View[S] from (data, rootOff). Callers
// that need v1 Object accessors can call zap.WrapBuffer on the
// returned data slice.
Wrap func(b []byte) (data []byte, rootOff int, err error)
}
Entry describes how to handle one schema. The Wrap closure parses a buffer and returns the validated data slice + root offset, reflection-free.
Name and Kind are duplicated from the Schema for convenient inspection without instantiating the Schema receiver.
type Field ¶
type Field[S Schema, T FieldKind] struct { Offset uint32 // contains filtered or unexported fields }
Field is a phantom-typed accessor for a fixed-size field within schema S. The type parameter T is the field's wire type (one of the FieldKind union). At compile time:
- Read[S, T](View[S], Field[S, T]) returns a T.
- Read[S, T](View[S], Field[OtherSchema, T]) is a compile error.
- Write[S, T](Setter[S], Field[S, T], v) writes v at the field's offset.
- Write[S, T](Setter[OtherSchema], ...) is a compile error.
The runtime representation of Field is just an offset; the compile- time tag is free. This is what gives v2 zero abstraction cost versus the v1 hand-rolled offset constants.
Construct Field values once at package init (see the canary AdvanceTime schema in examples/advance_time_tx.go for the pattern). Reusing Field values across goroutines is safe — they are immutable after declaration.
Implementation note: Go 1.23+ does not allow type parameters on methods of generic types ([View[S].Read[T] would be illegal]). v2 therefore exposes Read and Write as top-level generic functions rather than methods on View / Setter. The call site is at least as terse as a method call: `zapv2.Read(view, F.Time)`.
func At ¶
At returns a Field[S, T] for the given byte offset within S's fixed-size payload. Construct fields once per schema:
var AdvanceTimeFields = struct {
Time zapv2.Field[AdvanceTimeSchema, uint64]
}{
Time: zapv2.At[AdvanceTimeSchema, uint64](1),
}
The offset is in bytes. The caller is responsible for keeping offsets within S{}.Size() and for not overlapping fields — this is a schema-design concern, not enforced at runtime. (A misdeclared offset is caught by the byte-equality test in the canary, which is the right safety net — wire format is the source of truth.)
type FieldKind ¶
type FieldKind interface {
~bool |
~int8 | ~int16 | ~int32 | ~int64 |
~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64
}
FieldKind is the type-level constraint a Field's value type must satisfy: any Go value type that can be stored in a fixed-size slot. The exhaustive set is the integer + float + Address + Hash + bool family; variable-length values (bytes, text, lists, sub-objects) have their own accessors because their wire representation is a pointer pair, not a primitive.
We use a comparable union so the generic Field type is constrained at compile time. Users who add new fixed-size element types extend this constraint; no other API surface changes.
type KindByte ¶
type KindByte uint8
KindByte is the one-byte discriminator at offset 0 of every v2 object payload. It identifies the schema concretely.
In v1 this lived as `TxKind uint8` per-package; v2 lifts it into the ZAP layer so the generic View, Wrap and Registry code can reason about kind without importing every consumer.
type List ¶
type List[E Schema] struct { // contains filtered or unexported fields }
List is a typed, zero-copy view into a homogeneous list of objects of schema E. List[E] and List[F] are distinct types at compile time; you cannot accidentally iterate a list of Outputs as if it were a list of Inputs.
The element type E identifies the schema for each entry. Each call to List.At returns a View[E] over the corresponding fixed-size slot, and List.All returns an iter.Seq for range-over-func.
Layout: like View, List is value-typed and contains no pointers other than the slice header for `data`. It carries the absolute byte offset of the first element and the wire-encoded length. Stride derives from E{}.Size() at call time.
func ListAt ¶
ListAt reads a List[E] from the field at offset f in view v.
Top-level function (not a method on View[S]) for the same reason as Read: Go 1.23 does not allow type parameters on methods of generic types.
E's schema-Size() is used as the per-element stride hint, which activates the tighter wire-layer bounds check (zap.Object.ListStride) — an attacker-set 0xFFFFFFFF length is rejected up front.
func (List[E]) All ¶
All returns an iter.Seq over every element. Use with the Go 1.23+ range-over-func form:
for view := range list.All() {
t := zapv2.Read(view, EFields.Time)
// ...
}
Constant memory: the iterator does not materialize a slice; each View[E] aliases the corresponding slot in the underlying buffer. Verified by [bench_test.go] over 1M elements.
The yield function can return false to early-exit; semantics are identical to the stdlib iter.Seq contract.
Performance: View[E] is 56 bytes (three slice headers + an int32), so the closure spills it to stack each iteration. For the hot path where you only need the payload (e.g. fixed-field reads), use List.Payloads which yields a 24-byte slice per element — that fits in registers and matches v1's per-element iteration cost.
func (List[E]) At ¶
At returns the View[E] for element i. Out-of-range i returns the zero View[E] (matches v1 graceful degradation).
Element stride is taken from E{}.Size() — every element in a homogeneous list occupies exactly that many bytes.
func (List[E]) Indexed ¶
Indexed returns an iter.Seq2 with (i, View[E]) pairs. Useful when the index is part of the loop logic (e.g. paired with a sibling slice of expected values in a test).
func (List[E]) Len ¶
Len returns the number of elements in the list.
SAFETY: the value is wire-encoded; callers MUST NOT pre-allocate `make([]T, l.Len())` without an independent upper bound — see the v1 documentation of zap.List.Len for rationale. Iterate via [All] or index via At instead.
func (List[E]) Payloads ¶
Payloads returns an iter.Seq over the per-element payload slices — a hot-path variant of List.All that yields only the 24-byte payload slice rather than the full 56-byte [View[E]]. The element's fixed fields are accessible via ReadPayload; variable-length tails (sub-objects, lists, bytes) are NOT — for those, use [All].
Why it exists: View[E] doesn't fit in registers, so iterating a large list via [All] spills to stack per element (~2 ns extra). Payloads yields a slice header (16 bytes data ptr + len), which the compiler keeps in registers. For 1000-element batches reading only fixed fields, this closes the gap to v1's hand-rolled list iteration. See [bench_test.go] for the side-by-side.
Usage:
for payload := range list.Payloads() {
v := zapv2.ReadPayload[ItemSchema, uint64](payload, ItemFields.Value)
// ...
}
type Pool ¶
type Pool[T any] struct { // contains filtered or unexported fields }
Pool is a generic, type-safe sync.Pool wrapper. Instead of writing a dedicated sync.Pool + Get + Put per type as v1 did, register a constructor once:
var ViewPool = zapv2.NewPool(func() *View[AdvanceTimeSchema] {
return &View[AdvanceTimeSchema]{}
})
v := ViewPool.Get()
defer ViewPool.Put(v)
Get returns a *T; Put returns it to the pool. The generic guarantees that a value pulled from Pool[A] cannot be returned to Pool[B] (would not type-check).
Pool itself is safe for concurrent use (delegated to sync.Pool).
func NewPool ¶
NewPool returns a Pool[T] whose New func is `new`. The constructor is called whenever the pool is exhausted. Always return a fresh, zero-state *T from new — never a stale value.
type Raw ¶
type Raw struct {
// contains filtered or unexported fields
}
Raw is the non-generic shape of View: same three fields, no schema type parameter. It exists as the value-shape that the per- schema Wrap shims construct in their hot path. Construction is via WrapRaw / WrapRawUnchecked; the public typed View[S] wraps a Raw with one struct cast.
Treat Raw as an implementation detail of the per-schema shim constructors — application code uses [View[S]] / Wrap.
func RawFromSlices ¶
RawFromSlices composes a Raw directly from already-validated slices. Used by per-schema [WrapX] shims that did the parse + kind check inline — they pass the buffer, the root offset, and the end offset, and get back a Raw with the payload slice carved out (with the three-index slice form so accidental appends on the payload don't bleed into adjacent message bytes).
SAFETY: the caller MUST have verified rootOff and end fall within data — this constructor performs no bounds checks. It is a struct- literal compose, intentionally so the inliner folds it into the per-schema shim.
func WrapRaw ¶
WrapRaw parses a ZAP buffer and returns a Raw (non-generic) with the kind byte at the root validated against wantKind. The buffer's fixed-section size is validated against size. The schema name is used only when constructing a *SchemaError on kind mismatch.
Used by per-schema [WrapX] shims: they pass S{}.Kind(), S{}.Size(), S{}.Name() as constants (because those methods on a zero-sized struct fold to literals at the call site), then convert the Raw to View[S] via AsView. Because none of the parse + bounds work is inside a generic shape function, the whole pipeline collapses to inlineable v1 primitives in the caller's frame — matching v1 hand-rolled performance to within compiler noise.
External callers should prefer [Wrap[S]] (which has the same wire semantics but goes through generic dispatch and is one function call slower per Wrap).
Implementation: ParseHeader is delegated as the only non-inlined call site. The remainder lives inline (kind check + bounds + Raw compose). The result is ONE function call per Wrap (the parseHeaderImpl trampoline inside ParseHeader) — same depth as v1 hand-rolled.
func WrapRawUnchecked ¶
WrapRawUnchecked is WrapRaw without the kind-byte check. The caller must have already dispatched on kind (typically via Registry.Lookup → Entry.Wrap).
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is a kind-byte → schema-typed wrapper dispatcher. Each registered schema produces an Entry that knows, at runtime, how to wrap a buffer as the right typed View. The Entry itself was generated at compile time when Register was called, so dispatch requires no reflection and no per-call allocation — only a map lookup.
Registry is safe for concurrent reads; Register is intended for use at package init and is guarded by a mutex against concurrent init-time races.
Hickey decomposition: Registry is a value (map of values). The "what kind is this?" question is answered by looking at a byte; the "how do I parse it?" question is answered by an Entry value; there is no inheritance, no interface tower, no "TxFactory".
type Schema ¶
type Schema interface {
// Kind returns the discriminator byte at object offset 0.
Kind() KindByte
// Size returns the fixed-size object payload in bytes (the value
// passed to v1's StartObject). Variable-length tails (text, bytes,
// lists) are appended after this fixed section and do not count.
Size() int
// Name returns a human-readable schema name. Used for error
// messages and for [Registry] introspection. MUST be stable across
// versions — it is part of the schema's identity, not its display.
Name() string
}
Schema describes a v2 message shape. A type implements Schema by declaring its kind byte, its fixed object size (in bytes within the data segment), and a human-readable name.
A schema is a value, not a place. Implementations are typically zero-sized marker structs whose name documents the wire shape:
type AdvanceTimeSchema struct{}
func (AdvanceTimeSchema) Kind() KindByte { return 0x14 }
func (AdvanceTimeSchema) Size() int { return 9 }
func (AdvanceTimeSchema) Name() string { return "AdvanceTimeTx" }
Implementations MUST be safe to use as the zero value of their type — the methods consult only the receiver type, never any state. This invariant is what lets Registry.Register and reflect-free generic dispatch work.
type SchemaError ¶
SchemaError is returned when a buffer does not match the expected schema (wrong kind byte, undersized object payload, etc).
func NewSchemaError ¶
func NewSchemaError(want, got KindByte, name string) *SchemaError
NewSchemaError is the exported constructor used by per-schema [WrapX] shims (hand-written or codegen'd) on the kind-mismatch error path. Application code typically does not call this directly — it ships through the per-schema Wrap function's return value.
Allocates one *SchemaError; only called on the error path so the alloc cost is outside the hot-path budget.
func (*SchemaError) Error ¶
func (e *SchemaError) Error() string
type Setter ¶
type Setter[S Schema] struct { // contains filtered or unexported fields }
Setter is the write-side counterpart to View. A Setter[S] writes fields into the buffer for schema S. Like View, Setter is phantom- typed: a Field[S, uint64] cannot be written through Setter[T].
Setter holds two pointers (ob, b) — both into the active builder. These pointers' targets always outlive the Setter (the builder owns them and is itself reachable from the caller's stack). Setter values are NOT safe to retain after the builder's Finish returns.
type View ¶
type View[S Schema] struct { // contains filtered or unexported fields }
View is a typed, zero-copy reference into a ZAP buffer. The type parameter S identifies the schema; a View[A] and a View[B] are distinct types and the compiler refuses to mix them. This is what makes Field accessors safe: the compiler will not let you pass a Field[A, uint64] to a View[B].Read.
View carries no per-instance schema state — the schema's identity lives in the type parameter, and S{}'s methods (Kind/Size/Name) are queried only at boundaries (Wrap, Build, Registry). Inside the hot path the type parameter is purely a compile-time tag; no boxing, no interface dispatch.
View is safe to copy. The underlying buffer must outlive every View that references it.
Layout: a View holds two slice headers — `data` aliases the full ZAP buffer (used by variable-length tail accessors: list, bytes), and `payload` aliases the fixed section of the root object (used by Read/Write for direct unsafe-pointer indexing). Neither field is a pointer to a heap-allocated `*zap.Message` — that was the v1.0 design and forced a heap allocation per Wrap because the *Message escaped from the View's lifetime. The v1.1 redesign keeps everything in slice headers; View[S] is 56 bytes (3 slice headers + an int32 offset); stack-allocatable; escape-analysis-friendly.
func AsView ¶
AsView converts a Raw to a typed [View[S]] without copying. The caller asserts that the Raw was produced by a constructor matching schema S (typically a per-schema [WrapX] shim that pinned S{}.Kind() and S{}.Size() at compile time).
This is a zero-instruction cast: View[S] and Raw share an identical memory layout (data, payload, rootOff). The compiler folds it away via unsafe.Pointer reinterpretation — there is no struct copy. Verified by [bench_test.go].
Layout invariant: View[S] MUST have the exact same fields in the same order as Raw. The compile-time assertion in [init] pins this.
func Build ¶
Build writes a fresh ZAP buffer carrying a View[S], populated by the provided init function. The init function receives a Setter[S] — the only handle through which fields may be written — and is expected to set every field the schema requires before returning.
The schema's kind byte is written at object offset 0 before init is called; init MUST NOT overwrite offset 0.
Returns both the typed View and the underlying byte buffer. The two alias the same memory; the buffer is what goes on the wire.
Performance: Build pays the closure-allocation cost (the init func captures its receiver and any callsite variables). For the per-tx hot path use the imperative Builder[S] / WriteB pair instead — it matches the v1 hand-rolled 1-alloc profile.
func Wrap ¶
Wrap reads a ZAP buffer and returns a typed View[S].
The kind byte at object offset 0 is checked against S{}.Kind(); a mismatch returns a *SchemaError. The underlying ZAP frame is validated by zap.Parse (magic, version, size, header-alias safety); any of those failures propagate as their original error.
Zero copy: the returned View references b directly. The caller MUST keep b alive while the View is in use.
Performance: View[S] is value-typed and contains no pointers. After generic instantiation + inlining, this collapses to a single Parse call (which the v1 layer inlines further), a 1-byte read for the kind check, and three slice-header writes. No heap allocation — verified by [bench_test.go].
func WrapAs ¶
WrapAs is the typed-dispatch form: given a buffer of unknown kind and a target schema S, wrap if-and-only-if the buffer's kind matches S{}.Kind(). Equivalent to Wrap for callers who already have S.
Existence note: kept alongside Wrap for symmetry with the Registry-driven flow (PeekKind → branch → WrapAs).
func WrapUnchecked ¶
WrapUnchecked is Wrap without the schema discriminator check. It still validates the ZAP frame. Intended only for callers that have already dispatched on kind (e.g. inside [Registry.Wrap]).
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
This file deliberately fails to compile.
|
This file deliberately fails to compile. |
|
Package codegen emits per-schema ZAP v2 accessors that match v1's hand-rolled inline-everything performance.
|
Package codegen emits per-schema ZAP v2 accessors that match v1's hand-rolled inline-everything performance. |
|
cmd/zapgen
command
Command zapgen emits per-schema ZAP v2 accessors from a declarative schema description.
|
Command zapgen emits per-schema ZAP v2 accessors from a declarative schema description. |
|
cmd/zapgen-all
command
Command zapgen-all bulk-emits per-schema ZAP v2 accessor files from a directory of JSON schema declarations.
|
Command zapgen-all bulk-emits per-schema ZAP v2 accessor files from a directory of JSON schema declarations. |
|
Package examples contains worked examples that demonstrate how to express a schema with the v2 generic ZAP API.
|
Package examples contains worked examples that demonstrate how to express a schema with the v2 generic ZAP API. |