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 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:
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:
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
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.
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
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:
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:
{
"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
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:
// 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" |