# Json

> A member that holds a JSON document — an object, an array, or any value. It is stored as real jsonb, and written and read through JsonSerializer, so any value you can serialize goes in. Reach for it when the shape genuinely varies; declare real properties when it does not.

<!-- id: types-json · area: types · stability: stable · html: https://osysharp.com/reference/types/json/ -->

## Summary        {#summary}
A **`Json`** member holds a JSON document — an object, an array, a number, whatever the row needs. It is stored as
**real `jsonb`**, so the database holds structure rather than a blob of text.

```osy syntax
entity Job {
  [Required, MaxLength(50)] string Name;
  Json Detail;                              // whatever this job's handler wants to record
}
```

There are two ways in, and the shape of your data picks between them. When the document has a shape worth naming,
declare a `class` and use [JsonSerializer](https://osysharp.com/reference/json/serializer/)'s `Serialize` — it takes **any value**: a scalar, a list, a map, a class
instance, an entity. When the shape is decided at the call site, write it inline as `new { … }`, which is itself a
`Json` value. Either way you read it back with `Deserialize`.

## Signature      {#signature}
```osy syntax
Json  Detail;     // required — a JSON document has no honest empty value
Json? Detail;     // optional — absent until something writes it
```

## Description    {#description}

### How do I write a document and read it back?     {#round-trip}
The pair is the whole surface, and it is the ordinary C# shape. A `class` is the usual choice when the document has a
shape worth naming — but it is a choice, not a requirement:

```osy title="serialize a class in, deserialize it back out" syntax
class Detail { public int Attempt; public string Reason; }

entity Job { [Required, MaxLength(50)] string Name; Json Detail; }

void Record() {
  var j = new Job { Name = "j1", Detail = JsonSerializer.Serialize(new Detail { Attempt = 2, Reason = "retry" }) };
}

int Attempts(Job job) {
  return JsonSerializer.Deserialize<Detail>(job.Detail).Attempt;
}
```

**Anything serializable goes in**, not just a class:

```osy title="an array, a scalar, an entity — all go in" syntax
Json Tags    = JsonSerializer.Serialize(["urgent", "billing"]);   // an array
Json Reading = JsonSerializer.Serialize(42);                       // a scalar
Json Row     = JsonSerializer.Serialize(order);                    // an entity, as its shallow shape
```

⚠ **The compiler does not check the text, but the database does.** A `Json` column is `jsonb`, so Postgres rejects
anything that is not well-formed JSON — the failure arrives at the WRITE, not at the compile. Assembling the text by
hand is therefore not merely awkward, it is how you get a runtime error out of a program that compiled; `Serialize`
cannot produce malformed output.

### When the shape is decided at the call site: `new { … }`     {#object-literal}
A `class` is the right answer when the document has a shape worth naming and reusing. When it does not — a diagnostic
detail, a webhook body you are assembling, one row's worth of context — write the document inline:

```osy title="a shape with no name, written at the call site" syntax
entity Job { [Required, MaxLength(50)] string Name; Json Detail; }

void Record(int attempt, string reason) {
  var j = new Job { Name = "j1", Detail = new { attempt = attempt, reason = reason, at = DateTime.UtcNow } };
}
```

A name can be left out only where the value supplies one — `new { v.Label }` means `Label = v.Label`. A bare local
does not, and the compiler says so rather than inventing a key.

`new { … }` **is** a `Json` value — Osy# has no anonymous types, it has a document type — so it goes anywhere a
`Json` is accepted: a property, a local, an argument, a return.

**Documents nest**, and a nested one stays a document rather than becoming a quoted string:

```osy title="a nested document stays a document" syntax
Json outer = new { code = "E17", inner = new { retries = 3, fatal = false } };
// → {"code":"E17","inner":{"retries":3,"fatal":false}}
```

Two things the compiler refuses, both because a document has one value per key: **a repeated key**
(`new { a = 1, a = 2 }`), and **the dictionary spelling** (`new { ["a"] = 1 }` — a document key is a name).

⚠ **It is not an escape from the type system.** A document is a `Json` value and nothing else, so
`Runs = new { a = 1 }` on an `int` column is refused exactly as any other type mismatch would be.

### It is required by default, like every type with no honest zero     {#required}
A number defaults to `0` and a `bool` to `false`, and those are real answers. A document has no equivalent, so a bare
`Json` member is **required** and a `new Job { … }` that omits it is refused. Write `Json?` when a job may genuinely
not have one yet — see [Optional and required members](https://osysharp.com/reference/types/optional-and-required/).

### How do I cap how big a document may get?     {#size}
A JSON member has no natural limit, so `[MaxBytes]` is how you give it one:

```osy syntax
[MaxBytes(1048576)] Json Payload;      // at most 1 MB of stored JSON
```

Worth doing on anything an outside system writes into. See [constraints](https://osysharp.com/reference/entity/constraints/).

### When NOT to use it     {#when-not}
The storage is structured, but **Osy# has no way to reach inside it**: you cannot filter, sort or group by something
within the document, secure a field of it, or bind one to a grid column. What you can do is read the whole document
out and deserialize it. So a `Json` member is not a shortcut for properties you were going to query.

Declare real properties whenever the shape is known — even a long one. Reach for `Json` when it genuinely varies per
row: a webhook body you received, a provider-specific configuration block, a handler's own diagnostic detail. If you
find yourself deserializing to the same class everywhere and filtering its fields in memory, those fields wanted to be
properties.

## Examples       {#examples}

Recording a provider-specific configuration whose shape differs per provider, with the known parts declared and only
the varying part left as a document:

```osy title="provider-config" test app=types-json-example
enum Provider { GitHub, GitLab }

class GitHubConfig { public string Owner; public string Repo; public bool UseChecks; }

entity Connection {
  [Required, MaxLength(100)] string Name;
  Provider Provider;                             // declared — queried, filtered, shown
  [MaxBytes(65536)] Json ProviderConfig;         // varies per provider
}

void ConnectGitHub(string name, string owner, string repo) {
  var c = new Connection {
    Name           = name,
    Provider       = Provider.GitHub,
    ProviderConfig = JsonSerializer.Serialize(new GitHubConfig { Owner = owner, Repo = repo, UseChecks = true }),
  };
}
```

`Provider` is a real property because every connection has one and you will filter on it. `ProviderConfig` is a
document because GitHub's settings and GitLab's have nothing in common.

The same job when the shape is one call site's business — a failure record nobody else consumes, so declaring a class
for it would be ceremony:

```osy title="inline-document" test app=types-json-inline
entity Delivery {
  [Required, MaxLength(100)] string Endpoint;
  int Attempts;
  Json? LastFailure;                             // absent until something goes wrong
}

void RecordFailure(string endpoint, int status, string body) {
  var d = Delivery.Where(x => x.Endpoint == endpoint).First();
  d.Attempts = d.Attempts + 1;
  d.LastFailure = new {
    status  = status,
    body    = body,
    attempt = d.Attempts,
    context = new { endpoint = endpoint, at = DateTime.UtcNow },
  };
}
```

`context` nests as a document, not as a quoted string — so a consumer reading `LastFailure` back gets structure all
the way down.

## See also       {#see-also}
- [JsonSerializer](https://osysharp.com/reference/json/serializer/) — `Serialize` / `Deserialize`, which are how a `Json` member is written and read
- [Markdown](https://osysharp.com/reference/types/markdown/) — the other member type whose value is a document rather than a scalar
- [Optional and required members](https://osysharp.com/reference/types/optional-and-required/) — why a bare `Json` is required, and when to write `Json?`
- [constraints](https://osysharp.com/reference/entity/constraints/) — `[MaxBytes]`, for bounding a document's stored size
- [entity members](https://osysharp.com/reference/entity/properties/) — declaring members in general
