One JSON Schema to Rule Your Identity Traits: Ory Kratos's Identity Schema in Practice
Ory Kratos uses one JSON Schema document as the source of truth for identity traits, driving both server-side validation and self-service form rendering. Here's how it works, with a worked example and the migration trap to avoid.
12 Aug 2025, 10:04 UTC

Most identity systems split their data model into at least three places: a database column definition, backend validation code, and whatever the registration form happens to ask for. They drift. Someone adds a required field to the API but forgets the mobile form, or the UI collects a trait the backend silently drops. Ory Kratos takes a different position: a single JSON Schema document — the identity schema — is the source of truth for what an identity looks like, and it drives both server-side validation and the shape of self-service registration and login forms.
The thesis of this post is simple: treating your identity model as a schema, rather than as scattered code, is the most consequential design decision Kratos makes — and it's worth understanding before you write any integration code. It's also version-sensitive, so I'll flag where you need to check your own release.
What the identity schema actually is
The identity schema is a JSON Schema document you register with Kratos. It defines traits — the attributes attached to an identity, like an email address, a display name, or an employee ID. Traits are the data you own; credentials (passwords, WebAuthn keys, OIDC links) are managed separately by Kratos and are not part of your schema.
Because the schema is standard JSON Schema, you get the usual vocabulary: types, formats, required fields, enums, min/max lengths. Kratos adds its own extension annotations (under an ory.sh/kratos key) to mark which trait acts as the login identifier for a given credential type — for example, flagging the email field as the identifier for password login and as the address for recovery and verification emails.
Why one schema beats three sources of truth
The practical payoff shows up in the self-service flows. Kratos is API-first: when a user starts registration, the server returns a UI node tree — a structured description of the form fields, their labels, their current values, and any validation errors. Your web app, your mobile app, and your CLI all render that same tree. Because the tree is generated from the identity schema, a change to the schema propagates to every client without a client release.
Concretely, that means:
- Validation lives in one place. Mark a trait as required in the schema and Kratos rejects incomplete submissions server-side, while also surfacing the error in the node tree for the UI to display.
- No duplicated trait logic. Your React app and your iOS app don't each re-implement "department must be one of these five values."
- Per-identity-type customization. Kratos supports multiple schemas (for example, a "customer" schema and an "employee" schema with different traits), selected per flow. The exact mechanics vary by version, so confirm against your release's documentation.
A worked example: email plus a custom trait
Here's a minimal identity schema with a required email (used as the password-login identifier and for verification/recovery) and a custom department trait constrained to an enum:
{
"$id": "https://example.com/schemas/customer.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Customer",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "Email",
"ory.sh/kratos": {
"credentials": {
"password": { "identifier": true }
},
"verification": { "via": "email" },
"recovery": { "via": "email" }
}
},
"department": {
"type": "string",
"title": "Department",
"enum": ["engineering", "sales", "support"]
}
},
"required": ["email"],
"additionalProperties": false
}
}
}You point Kratos at this document in its configuration (the config key for identity schemas has been stable, but check the exact syntax for your version — file references, URLs, and base64 inline forms have all existed). After restarting Kratos, start a registration flow against the self-service API and inspect the response: the UI node tree should contain an email input, a department input, and a password field, with the email marked required. Submitting a payload with department: "legal" should come back with a validation error attached to that node rather than a generic 400 — that's the schema doing the work end to end.
To verify this yourself, you need network access to your Kratos instance and no special privileges beyond the public self-service endpoints. The check is: does the returned node tree match the schema's fields, and does an out-of-enum value produce a field-level error? If either fails, the schema likely wasn't loaded — check Kratos's startup logs, which report schema loading problems.
The trade-off: schemas are easy to write and risky to change
Here's the part that bites people. The schema is enforced against existing identities too, not just new registrations. If you add "department" to the required list after you already have ten thousand identities without one, those identities may suddenly fail validation — for example, when a user tries to update their settings and the whole traits document is revalidated. The same applies to tightening a format or narrowing an enum.
So schema evolution is a migration problem, not a config change. A safe pattern is:
- Add new traits as optional first.
- Backfill existing identities (via the admin identity API or a batch job against your data).
- Only then make the trait required, after verifying zero identities fail validation.
Also note that the supported schema features — multiple identity schemas, schema update semantics, which JSON Schema draft features are honored — differ across Kratos versions. Before relying on any of the above, confirm your version with kratos version (or the version endpoint on the admin API) and read that release's identity-schema documentation. Treat anything older than your installed version as a hint, not a guarantee.
Where to start
Take your current registration form, write it down as a JSON Schema with the Kratos extension annotations, and load it into a local Kratos instance (the quickstart Docker setup is enough). Run one registration through the API and read the node tree you get back. That single round trip — schema in, form and validation out — will tell you more about whether this model fits your product than any amount of architecture reading. Then, before your first production schema change, rehearse the optional-then-backfill-then-required migration on staging. Your future self, staring at a settings page full of validation errors, will thank you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.