Expand description
One schema per type, and every projection reads it.
A CanonicalSchema is a JSON Schema (draft 2020-12, as schemars emits it) derived
from the Rust type that the handler actually deserialises and serialises. That is the
whole architectural point: MCP’s inputSchema, the OpenAPI operation’s parameters and
its components/schemas entry, and the Cockpit runner’s form are four renderings of
this value, produced by CanonicalSchema::for_mcp and
CanonicalSchema::for_openapi. None of them describes a payload of its own, so none
of them can describe it differently from the code that reads it.
§The two dialects
MCP takes the schema inline. OpenAPI wants named components and $refs, so
CanonicalSchema::for_openapi hoists the type into a components map under the
name schemars gave it and answers with the reference — which is why the component
namespace is flat and why two types with one title are a build-time conflict rather
than a document that silently describes one of them twice.
§One shape under two names
A #[serde(transparent)] newtype — a module’s input named for its operation over a
domain type named for the domain — has a schema whose root is a $ref into its own
$defs. CanonicalSchema::resolved follows that reference, and every projection
that binds properties reads it through CanonicalSchema::properties, so the wrapper
names the component and the inner type supplies the shape.
§Query strings
An HTTP GET binds each top-level property from the query string, where everything is
a string. coerce is the one place that turns "12" into a number and "true" into
a boolean, guided by the property’s own schema, so a route cannot invent a coercion the
schema does not describe.
use majordomus_cli::capability::CanonicalSchema;
use schemars::JsonSchema;
/// A search over the index.
#[derive(JsonSchema)]
struct SearchInput {
/// What to look for.
query: String,
/// How many hits at most.
limit: u32,
}
let schema = CanonicalSchema::of::<SearchInput>();
assert_eq!(schema.name.as_deref(), Some("SearchInput"));
// the properties every projection binds, derived once
let (properties, required) = schema.properties();
let names: Vec<&str> = properties.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"query") && names.contains(&"limit"));
assert!(required.contains(&"query".to_string()));
// MCP takes it inline; OpenAPI takes a reference and the component beside it
assert!(schema.for_mcp()["properties"]["query"].is_object());
let mut components = std::collections::BTreeMap::new();
let reference = schema.openapi_ref(&mut components).unwrap();
assert_eq!(reference["$ref"], "#/components/schemas/SearchInput");
assert!(components.contains_key("SearchInput"));Structs§
- Canonical
Schema - A JSON Schema with, when the type has one, a stable component name (the type’s title).
Functions§
- coerce
- Convert a query-string value to the JSON type a property schema names: integers and booleans are parsed, a structured parameter is read as the JSON text the binding sent, and everything else stays a string.