Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Types

Json

Json Detail; // a member holding a JSON document

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.

stable2 examples compiled by CItypesentityauthoring

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 it

Description#

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 JSON

Worth 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#

Related

JsonSerializer

Turn a value into a JSON string and a JSON string into a typed object — the C#-faithful System.Text.Json spelling…

Markdown

A member that holds a markdown document. You read and write it as ordinary text, but it is stored as a list of sections…

Optional and required members

A member is required or optional by how you spell its type. A bare value type with a natural zero reads that zero; a…

entity members

The typed members an entity holds — text, numbers, dates, booleans, Guids, enums and references. A member's type…

constraints

The per-member rules the database enforces — Required, Unique, MaxLength/MinLength, Min/Max, Pattern, and the…