Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne program

Reference / Function

How deep can an object graph get?

500 levels — the limit on any value serialized across an `await` or through JsonSerializer

A value built out of references to other values — a linked list, a tree, a chain of parents — may nest up to 500 levels. Past that, serializing it across a suspend or through JsonSerializer.Serialize is refused with an error naming the depth and the class. A cycle is fine across a suspend and refused by JsonSerializer.

stable2 examples compiled by CIruntimeserializationlimitsclass

Summary#

A class whose field is another instance of the same class lets you build a chain, a tree, or any other graph of references, as deep as your data goes. Two things later have to turn that graph into one document — parking it across an await, and JsonSerializer — and both cap it at 500 levels of nesting.

Under 500, nothing about this is visible. Past it you get an error naming the depth, the limit, and the class the walk was inside when it stopped.

Signature#

// 500 levels of nesting, counting the value itself as level 1
class Node { public int V; public Node? Next; }   // Next → Next → Next → … up to 500

Description#

Which values does this cover?#

Any value whose nesting your DATA decides rather than your source: a class field pointing at another instance, a List of Lists, a Map whose values are maps. A field on an entity never counts — a relation is a FK, not a nested value, and serializing an entity is shallow by design (JsonSerializer).

Depth is counted the way you would read it: the value itself is level 1, the thing its field points at is level 2. A 500-link chain plus its terminating null is 501 levels, so the last link that fits carries 499 before it.

Where does the limit apply?#

Two places, both of which turn a live graph into one stored document:

WhereWhat happens past 500
A value held across an await that suspends (a workflow parking, a client hand-off, a durable step result)the suspend fails with a value held across a suspend is nested N levels deep — the limit is 500 (reached inside class 'X')
JsonSerializer.Serialize(value)the value passed to JsonSerializer.Serialize is nested N levels deep — the limit is 500 (reached inside class 'X')

Both are ordinary errors: they are raised where the serialization happens, they name the class, and code around them can catch them. Building the graph is never refused — only storing one that deep.

Why 500, and not "as deep as you like"?#

A parked continuation is persisted as one JSON document, and the reader that has to parse it back is bounded by the machine's stack whatever this platform does. 500 sits far enough below that ceiling to be safe on every thread the runtime uses, and far enough above any shape real data takes to be invisible.

If you are near it, the graph is almost certainly the wrong thing to be holding across the wait: park an id, or the handful of fields the code after the await actually reads, and re-derive the rest when it resumes. That is cheaper as well as shorter — the whole graph is written, stored and read back on every suspend.

What about a graph that points back at itself?#

The two surfaces answer differently, and both answers are deliberate.

Across an await, a cycle is fine. The durable form records object identity, so a value reachable twice is written once and referred to afterwards. A node whose Next is itself parks and resumes as a node whose Next is itself — one object, not a copy — and a change made through one path is seen through the other.

Through JsonSerializer.Serialize, a cycle is refused, with its own message rather than a depth one:

JsonSerializer.Serialize cannot serialize an instance of class 'Node', because it refers back to itself — JSON has
no way to write a reference to a value it has already written. Break the cycle before serializing (drop the
back-pointer, or serialize the id instead of the object).

JSON has no spelling for a back-reference, so there is nothing faithful to write. The same value appearing twice in different branches is not a cycle and is written out twice, as you would expect.

Examples#

class Node { public int V; public Node? Next; }

// Nothing here is checked at compile time: the expression is trivial and the depth is `n`, a runtime value.
Node? Build(int n) {
  Node? head = null;
  for (int i = 0; i < n; i++) { head = new Node { V = i, Next = head }; }
  return head;
}

string AsJson(int n) {
  return JsonSerializer.Serialize(Build(n));   // fine while n < 500; a named error past it
}
entity Batch { [Required, MaxLength(50)] string Code; int Size; }

class Crumb { public string Code; public int Size; }

// The few fields the code after the wait actually reads — flat, tiny, and immune to how deep the data got.
Crumb Summarize(Batch b) {
  return new Crumb { Code = b.Code, Size = b.Size };
}

Notes#

  • The limit is on NESTING, not on size. A list of a million flat items is depth 2 and stores fine; a chain of 501 is not, however small each link is.
  • Nothing about this is visible while you build the graph. It is a property of storing one, so the error appears at the await or the Serialize call, not where the data was assembled.

See also#

Related

JsonSerializer

Turn a value into a JSON string and a JSON string into a typed object — the C#-faithful System.Text.Json spelling…

constructor

A class declares one constructor — its name is the class name, it takes no return type, and it runs when you write new…

Workflow.Run (start a workflow)

Start the workflow bound to an entity's type, on that entity. Bare — `Workflow.Run(order)` — is fire-and-forget: start…