# Json.SchemaOf<T>() — the JSON Schema of a type

> The JSON Schema of an Osy# type, as text: a class is an object of its members, an enum is its members by name, a list is an array, a nullable member may be null. It describes exactly the document `JsonSerializer.Deserialize<T>` reads, so what a model, a tool or another system writes to it is something your code can read.

<!-- id: json-schema-of · area: json · stability: preview · html: https://osysharp.com/reference/json/schema-of/ -->

## Summary        {#summary}
**`Json.SchemaOf<T>()`** answers the [JSON Schema](https://json-schema.org/) (2020-12) of `T`, as a string:

```osy syntax
string schema = Json.SchemaOf<Quote>();          // {"type":"object","properties":{…},"required":[…],…}
```

You write the type once, as a class, and never write its schema beside it. Hand the schema to whatever will write
the JSON — a model asked for a structured answer, a tool's description, another system — and read what comes back
with [JsonSerializer](https://osysharp.com/reference/json/serializer/): `JsonSerializer.Deserialize<Quote>(text)`.

It is **pure** — no capability, no `using`.

## Signature      {#signature}
```osy syntax
string Json.SchemaOf<T>()     // T = a class, an enum, an entity, a built-in type,
                              //     or a List<T>, T[], HashSet<T> or Dictionary<string, T> of one
```

## Description    {#description}

### What each type becomes     {#shapes}

| The type | Its schema |
|---|---|
| a **class** | `{"type":"object","properties":{…},"required":[…],"additionalProperties":false}` — one property per member, in the order declared |
| an **enum** | `{"type":"string","enum":["Low","High"]}` — its members by name, or by `[ExternalName]` where one is declared |
| a `[Flags]` enum | `{"type":"integer"}` — a set of flags is a number; the description lists what each bit means |
| `List<T>`, `T[]` | `{"type":"array","items":…}` |
| `HashSet<T>` | the same, with `"uniqueItems":true` |
| `Dictionary<string, T>` | `{"type":"object","additionalProperties":…}` |
| `string` | `{"type":"string"}` — with `maxLength` where the member declares `[MaxLength]` |
| `int` · `long` | `{"type":"integer"}` · the same with `"format":"int64"` |
| `decimal`, `double` | `{"type":"number"}` |
| `bool` | `{"type":"boolean"}` |
| `DateTime` · `DateOnly` · `TimeOnly` | a string with `"format"` `date-time` · `date` · `time` |
| `TimeSpan` | a string as `[d.]hh:mm:ss` |
| `Guid` | a string with `"format":"uuid"` |
| `byte[]` | a string with `"contentEncoding":"base64"` |
| `Json` | `{}` — any JSON may stand there |
| an **entity** | an object of its id and its own values; a reference is the other row's id; its collections are left out |

**A member that can be null** says so in its type — `{"type":["string","null"]}` — and is not `required`. Every
other member is `required`. A member can be null when it is declared `T?` (`string? Note`, `Urgency? Fallback`), and
also when its type has no value that stands for "not set": a class, a `DateTime`, a `Guid`. Declare such a member
`required` (`public required Address ShipTo;`) to say it is always there.

**Names are the serializer's.** A member is named as [JsonSerializer](https://osysharp.com/reference/json/serializer/) reads and writes it: its `[ExternalName]`
where it declares one, its own name otherwise. So a document that satisfies the schema deserializes.

**Descriptions are your own words.** A `///` comment on a class or a member becomes that part's `description`, and
an enum's is its members' comments (`Low: Whenever there is time. High: Before the day ends.`). A model reads them,
so a sentence saying what a member is FOR is worth writing.

### A class inside itself     {#recursion}
A class that contains itself is written once and referred to. At the root it refers to itself as `#`:

```json
{"type":"object","properties":{"Name":{"type":"string"},"Children":{"type":"array","items":{"$ref":"#"}}}, …}
```

Anywhere else it is one definition under `$defs`, and every place it stands is `{"$ref":"#/$defs/Folder"}`.

### In a generic function     {#generic}
`Json.SchemaOf<T>()` works on a generic function's own type parameter, which is how a helper that asks for *any*
shape is written once:

```osy title="one function that asks for whatever shape its caller names" syntax
T Ask<T>(string question) {
  var answer = Model.Complete(question, schema: Json.SchemaOf<T>());   // your own call to a model
  return JsonSerializer.Deserialize<T>(answer);
}
```

The type is the one each call passes — `Ask<Quote>(…)`, `Ask<Urgency>(…)` — and the answer is the same text as
naming it directly. `JsonSerializer.Deserialize<T>` reads the same `T`.

A generic **class** reads its own type parameter the same way, in its instance members — the type the instance was
built over, so a subclass that closes the generic (`class Measure : Job<string, Reading>`) answers for `Reading`:

```osy title="a base class describing the type its subclass was built over" syntax
class Job<TIn, TOut> {
  public string Shape() { return Json.SchemaOf<TOut>(); }
  public TOut Read(string json) { return JsonSerializer.Deserialize<TOut>(json); }
}
```

Two limits, both said where they are met:

- The type must be a **named** one: a class, an enum, an entity or a built-in. For a list, name it where it is
  known (`Json.SchemaOf<List<Quote>>()`) or wrap it in a class.
- A `static` member of a generic class has no instance to read the type from. Take it as the member's own type
  parameter instead.

### On the client     {#client}
A schema of a type the call names is worked out when the app is compiled, so it is the same text in a function
that runs in the browser as in one that runs on the server. The generic form reads the app's model, so a function
using it runs on the server.

### What has no schema     {#refused}
A type that is not data is refused where it is written, naming what can be described:

```text
`Json.SchemaOf<Func<int>>()`: 'Func<int>' is not data, so JSON cannot carry it. A schema describes a class, an enum,
an entity, a built-in type (string, int, long, decimal, double, bool, DateTime, DateOnly, TimeOnly, TimeSpan, Guid,
byte[], Json), or a `List<T>`, `T[]`, `HashSet<T>` or `Dictionary<string, T>` of one.
```

A dictionary must be keyed by `string` — a JSON object's member names are text.

## Examples       {#examples}

### Describe a type, and read what was written to it     {#example-quote}
```osy title="a class, its schema, and reading a document written to it" test app=json-schema-of
public enum Urgency {
  /// Whenever there is time.
  Low,
  /// Before the day ends.
  High
}

public enum Unit { [ExternalName("kg")] Kilogram, [ExternalName("pcs")] Piece }

/// One priced line.
public class QuoteLine {
  /// What is being priced.
  public string Item = "";
  public decimal Price;
  public int Quantity;
  public Unit Unit;
}

/// A priced offer.
public class Quote {
  /// Who it is for.
  [ExternalName("customer_name")] public string Customer = "";
  public string? Note;
  public Urgency Urgency;
  public List<QuoteLine> Lines = [];
}

/// The schema to hand to whatever writes the JSON.
public string QuoteSchema() { return Json.SchemaOf<Quote>(); }

/// The same, for any type a caller names: `SchemaFor<Quote>()`, `SchemaFor<Urgency>()`.
public string SchemaFor<T>() { return Json.SchemaOf<T>(); }

/// What comes back, read as the type — `{"customer_name":"Ada","Urgency":"High","Lines":[…]}`.
public decimal Total(string json) {
  var quote = JsonSerializer.Deserialize<Quote>(json);
  decimal total = 0;
  foreach (var line in quote.Lines) { total = total + line.Price * line.Quantity; }
  return total;
}
```

`QuoteSchema()` answers:

```json
{
  "type": "object",
  "description": "A priced offer.",
  "properties": {
    "customer_name": { "type": "string", "description": "Who it is for." },
    "Note": { "type": ["string", "null"] },
    "Urgency": {
      "type": "string",
      "enum": ["Low", "High"],
      "description": "Low: Whenever there is time. High: Before the day ends."
    },
    "Lines": {
      "type": "array",
      "items": {
        "type": "object",
        "description": "One priced line.",
        "properties": {
          "Item": { "type": "string", "description": "What is being priced." },
          "Price": { "type": "number" },
          "Quantity": { "type": "integer" },
          "Unit": { "type": "string", "enum": ["kg", "pcs"] }
        },
        "required": ["Item", "Price", "Quantity", "Unit"],
        "additionalProperties": false
      }
    }
  },
  "required": ["customer_name", "Urgency", "Lines"],
  "additionalProperties": false
}
```

## See also       {#see-also}
- [JsonSerializer](https://osysharp.com/reference/json/serializer/) — reading and writing the document a schema describes
- [[Action] — what an app offers to be done, by name](https://osysharp.com/reference/function/action/) — an action's parameters and its answer, published as schemas
- [Json](https://osysharp.com/reference/types/json/) — the `Json` type, for a document with no fixed shape
