Summary#
Json.SchemaOf<T>() answers the JSON Schema (2020-12) of T, as a string:
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: JsonSerializer.Deserialize<Quote>(text).
It is pure — no capability, no using.
Signature#
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 oneDescription#
What each type becomes#
| 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 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#
A class that contains itself is written once and referred to. At the root it refers to itself as #:
{"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#
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:
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:
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
staticmember 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#
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#
A type that is not data is refused where it is written, naming what can be described:
`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#
Describe a type, and read what was written to it#
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:
{
"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#
- JsonSerializer — reading and writing the document a schema describes
- [Action] — what an app offers to be done, by name — an action's parameters and its answer, published as schemas
- Json — the
Jsontype, for a document with no fixed shape