AkurAI Build
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"` |