Skip to content

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:

ts
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 plain T.
ts
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)); // === json

Class-level modes ​

Set a default for a whole class with the @json config object, and override individual fields with @eager:

ts
@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 @omitif predicate 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/@deserializer can'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, or JSON.parse. A lazy field becomes a get/set accessor, and AssemblyScript's object-literal class init (const r: Repo = { … }) doesn't support accessors:

    ts
    const repo = new Repo();
    repo.name = "json-as";
    repo.owner = owner; // goes through the setter
  • A 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 @lazy over all.

  • 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.