Menu
Public repository / master
AkurAI-Framework / site/content/docs/schema.md
Latest change baca1a74b0ca9dcd770206285f8e51230b34d130 - feat(schema): add akurai-schema crate — JSON-shape validator for tool-UI matching by Ólafur Búi Ólafsson
# Schema
`akurai-schema` is a lightweight JSON-shape validator — the zero-dependency
substitute for Zod. It matches AI tool payloads to UI components by letting
you declare what shape a value must have, then validating any
[`akurai_json::Value`](./json.md) against that declaration.
---
## The schema model
A **`Schema`** describes an object (the root of every AI tool payload) as a
list of named fields. Each field has:
- a **name** (string key in the object)
- a **`FieldType`** (what kind of value is allowed there)
- a **required / optional** flag
Validation is exhaustive: every violation is reported, not just the first one.
Paths use dot-notation for nested objects (`user.address.city`) and
bracket-notation for array indices (`tags[2]`).
### `FieldType` variants
| Variant | Matches |
|---------|---------|
| `FieldType::String` | `Value::Str(_)` |
| `FieldType::Int` | `Value::Int(_)` |
| `FieldType::Float` | `Value::Float(_)` or `Value::Int(_)` |
| `FieldType::Bool` | `Value::Bool(_)` |
| `FieldType::Array(Box<FieldType>)` | `Value::Array(_)` where every element matches the inner type |
| `FieldType::Object(Box<Schema>)` | `Value::Object(_)` validated by a nested schema |
| `FieldType::Enum(Vec<String>)` | `Value::Str(s)` where `s` is one of the listed strings |
| `FieldType::Any` | any `Value`, including `Null` |
---
## Building a schema
Use `Schema::object` with a slice of `(name, FieldType, required)` triples:
```rust
use akurai_schema::{Schema, FieldType};
let schema = Schema::object(&[
("id", FieldType::Int, true),
("name", FieldType::String, true),
("score", FieldType::Float, false), // optional
("status", FieldType::Enum(vec!["active".into(), "inactive".into()]), true),
("tags", FieldType::Array(Box::new(FieldType::String)), false),
]);
```
Nested objects use `FieldType::Object`:
```rust
let address_schema = Schema::object(&[
("street", FieldType::String, true),
("city", FieldType::String, true),
]);
let schema = Schema::object(&[
("name", FieldType::String, true),
("address", FieldType::Object(Box::new(address_schema)), true),
]);
```
---
## `validate` and `matches`
### `Schema::validate`
```rust
pub fn validate(&self, value: &Value) -> Result<(), Vec<SchemaError>>
```
Returns `Ok(())` on success. On failure, returns `Err(errors)` where each
`SchemaError` has:
- **`path: String`** — dot/bracket path to the offending value (`""` = root)
- **`message: String`** — human-readable reason
All violations are collected in a single call — you get the complete picture.
```rust
use akurai_json::parse;
let schema = Schema::object(&[
("x", FieldType::Int, true),
("y", FieldType::String, true),
]);
let value = parse(r#"{"x":"bad","y":42}"#).unwrap();
let errors = schema.validate(&value).unwrap_err();
// errors[0].path = "x", errors[0].message = "expected integer"
// errors[1].path = "y", errors[1].message = "expected string"
```
### `Schema::matches`
```rust
pub fn matches(&self, value: &Value) -> bool
```
Convenience wrapper — `true` iff `validate` returns `Ok`. Use this when you
only need a yes/no answer (e.g. component selection in the tool-UI runtime).
---
## JSON schema description format
Schemas can be described as JSON so the same definition can be handed to a
client without recompilation. Use `Schema::from_json`:
```rust
pub fn from_json(value: &Value) -> Result<Schema, ParseError>
```
The JSON schema is an object whose **keys are field names** and **values are
field descriptors**:
```json
{
"name": { "type": "string", "required": true },
"count": { "type": "int", "required": true },
"score": { "type": "float", "required": false },
"active": { "type": "bool", "required": false },
"tags": { "type": "array", "items": "string", "required": true },
"status": { "type": "enum", "values": ["on", "off"], "required": true },
"addr": {
"type": "object",
"required": false,
"schema": {
"city": { "type": "string", "required": true }
}
},
"meta": { "type": "any", "required": false }
}
```
### Descriptor keys
| Key | Required | Meaning |
|-----|----------|---------|
| `type` | yes | One of `string`, `int`, `float`, `bool`, `array`, `object`, `enum`, `any` |
| `required` | no (default `false`) | Whether the field must be present |
| `items` | for `array` | Element type — a type string (`"string"`, `"int"`, …) or a full descriptor object |
| `values` | for `enum` | Array of allowed string literals |
| `schema` | for `object` | Nested JSON schema object |
Unknown keys in a descriptor are silently ignored — future extensions are
backward-compatible.
### Example — parse and validate
```rust
use akurai_json::parse;
use akurai_schema::Schema;
let schema_json = parse(r#"{
"id": {"type":"int", "required":true},
"name": {"type":"string", "required":true},
"tags": {"type":"array", "items":"string", "required":false}
}"#).unwrap();
let schema = Schema::from_json(&schema_json).unwrap();
let payload = parse(r#"{"id":1,"name":"button"}"#).unwrap();
assert!(schema.matches(&payload));
```
---
## Matching tool payloads to UI components
The tool-UI runtime uses `Schema::matches` to select the right component for
an AI tool's output. Each registered component carries a schema; the runtime
walks the registry and returns the first component whose schema matches the
incoming payload:
```rust
// Pseudo-code — actual wiring lives in the runtime layer.
fn find_component<'a>(registry: &'a [Component], payload: &Value) -> Option<&'a Component> {
registry.iter().find(|c| c.schema.matches(payload))
}
```
Because `matches` collects no allocations on the happy path (it short-circuits
via `validate_into` returning early once any error is found), selection over a
registry of dozens of components is negligible.
---
## Error path format
| Situation | Example path |
|-----------|-------------|
| Root value wrong type | `""` (empty string) |
| Top-level field wrong | `"name"` |
| Nested object field wrong | `"address.city"` |
| Three levels deep | `"order.shipping.zip"` |
| Array element wrong | `"tags[2]"` |
| Array-of-objects element field wrong | `"items[1].price"` |