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.
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'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#
Json Detail; // required — a JSON document has no honest empty value
Json? Detail; // optional — absent until something writes itDescription#
How do I write a document and read it back?#
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:
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:
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 { … }#
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:
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:
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#
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.
How do I cap how big a document may get?#
A JSON member has no natural limit, so [MaxBytes] is how you give it one:
[MaxBytes(1048576)] Json Payload; // at most 1 MB of stored JSONWorth doing on anything an outside system writes into. See constraints.
When NOT to use it#
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#
Recording a provider-specific configuration whose shape differs per provider, with the known parts declared and only the varying part left as a document:
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:
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#
- JsonSerializer —
Serialize/Deserialize, which are how aJsonmember is written and read - Markdown — the other member type whose value is a document rather than a scalar
- Optional and required members — why a bare
Jsonis required, and when to writeJson? - constraints —
[MaxBytes], for bounding a document's stored size - entity members — declaring members in general