Lazy Fields
A lazy field defers parsing. Instead of materializing the value during JSON.parse, json-as records the field's raw JSON slice and parses it only on first access. A field you never read is never parsed; on serialize, an untouched field's original bytes pass straight through. Access is fully transparent — a lazy field reads and writes exactly like a normal one.
This is ideal for large payloads where you touch only a few fields, and for proxy / filter / forward workloads.
Marking a field
Three equivalent ways, pick whichever reads best:
import { JSON } from "json-as";
@json
class Owner {
id: i32 = 0;
login: string = "";
}
@json
class Repo {
name: string = ""; // eager (parsed up front)
@lazy owner: Owner = new Owner(); // decorator form
files: JSON.Lazy<string[]> = []; // type-wrapper form
}@lazy— a bare decorator on the field.JSON.Lazy<T>— a transparent type alias (type Lazy<T> = T), so the field is still typed and used as a plainT.
const json =
'{"name":"x","owner":{"id":5,"login":"ada"},"files":["a.ts","b.ts"]}';
const repo = JSON.parse<Repo>(json);
repo.name; // "x" — parsed eagerly
repo.owner.id; // 5 — owner parsed now (on first read), then cached
// repo.files is never read here -> never parsed
// An untouched object round-trips by copying its original bytes:
JSON.stringify(JSON.parse<Repo>(json)); // === jsonClass-level modes
Set a default for a whole class with the @json config object, and override individual fields with @eager:
@json({ lazy: "auto" }) // "none" (default) | "auto" | "all"
class Doc {
id: i32 = 0; // auto: cheap -> stays eager
title: string = ""; // auto: deferred
tags: string[] = []; // auto: deferred
@eager hot: string = ""; // forced eager
}none(default) — only fields with an explicit marker are deferred.auto— defer the expensive-to-parse fields (strings, arrays, maps/sets,JSON.Value/Obj/Raw, non-trivial nested structs) and keep cheap ones eager (primitives, enums,Date, tiny all-scalar structs).all— defer every serialized field. Best for proxy / passthrough.
Precedence: explicit @lazy / JSON.Lazy<T> always wins, then @eager, then the class mode. @omit fields are never affected.
Interactions
@omitnull/@omitif— supported. Null-ness is decided from the stored slice without materializing the value; an@omitifpredicate that reads the field will materialize it, otherwise it passes through raw.- Generics — a lazy field on a generic class works for reference-type instantiations (e.g.
Box<Owner>); value-type instantiations (Box<i32>) are not supported. - Custom serializers — a class with a custom
@serializer/@deserializercan't have lazy fields (the custom methods replace the generated ones the slots rely on); the transform reports an error.
Caveats
Construct with
new+ assignment, orJSON.parse. A lazy field becomes a get/set accessor, and AssemblyScript's object-literal class init (const r: Repo = { … }) doesn't support accessors:tsconst repo = new Repo(); repo.name = "json-as"; repo.owner = owner; // goes through the setterA struct keeps its source string alive while it lives (the cost of zero-copy). For long-lived objects over huge payloads, read what you need and drop the object, or prefer per-field
@lazyoverall.lazy: "all"grows module size — it generates a getter and a serialize branch per field. When code size matters, mark only the fields you actually skip.
Performance
Lazy is fastest when you skip fields or forward them, and the win grows with payload size. See Performance for the eager-vs-lazy benchmarks.
