# How deep can an object graph get?

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

<!-- id: function-deep-object-graphs · area: function · stability: stable · html: https://osysharp.com/reference/function/deep-object-graphs/ -->

## Summary        {#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](https://osysharp.com/reference/json/serializer/) — 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      {#signature}
```osy syntax
// 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    {#description}

### Which values does this cover?   {#which-values}
Any value whose nesting your DATA decides rather than your source: a `class` field pointing at another instance, a
`List` of `List`s, 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](https://osysharp.com/reference/json/serializer/)).

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?   {#where}
Two places, both of which turn a live graph into one stored document:

| Where | What 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"?   {#why}
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?   {#cycles}
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:

```text
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       {#examples}

```osy title="a chain built in a loop — the shape that reaches the limit" test app=function-deep-object-graphs
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
}
```

```osy title="hold the id across the wait, not the graph" test app=function-deep-object-graphs
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          {#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       {#see-also}
- [JsonSerializer](https://osysharp.com/reference/json/serializer/) — the serialize surface this bounds, and what it does with each value shape
- [constructor](https://osysharp.com/reference/class/constructors/) — the `class` types whose fields make a graph nestable in the first place
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — the durable path a parked value has to survive
