use crate::schema::canonical;
use crate::schema::graph::SchemaGraph;
use crate::schema::host_managed::HostManagedKind;
use crate::schema::metadata::TypeId;
use crate::schema::render::error::RenderError;
use crate::schema::render::walker::{SchemaWalker, WalkerError, resolve_ref, walk};
use crate::schema::schema_type::{
DiscriminatorRule, ResultSpec, SchemaType, UnionBranch, UnionSpec, VariantCaseType,
};
use crate::schema::schema_value::{
ResultValuePayload, SchemaValue, UnionValuePayload, VariantValuePayload,
};
use serde_json::{Map, Number, Value};
use std::collections::HashSet;
pub fn to_json_value(
graph: &SchemaGraph,
ty: &SchemaType,
value: &SchemaValue,
) -> Result<Value, RenderError> {
let mut renderer = ToJsonRenderer {
path: PathStack::new(),
redact: false,
};
drive(walk(&mut renderer, graph, ty, value))
}
pub fn to_json_value_redacted(
graph: &SchemaGraph,
ty: &SchemaType,
value: &SchemaValue,
) -> Result<Value, RenderError> {
let mut renderer = ToJsonRenderer {
path: PathStack::new(),
redact: true,
};
drive(walk(&mut renderer, graph, ty, value))
}
pub fn from_json_value(
graph: &SchemaGraph,
ty: &SchemaType,
json: &Value,
) -> Result<SchemaValue, RenderError> {
from_json_value_with_policy(graph, ty, json, JsonDecodePolicy::Trusted)
}
pub fn from_untrusted_json_value(
graph: &SchemaGraph,
ty: &SchemaType,
json: &Value,
) -> Result<SchemaValue, RenderError> {
from_json_value_with_policy(graph, ty, json, JsonDecodePolicy::Untrusted)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum JsonDecodePolicy {
Trusted,
Untrusted,
}
fn from_json_value_with_policy(
graph: &SchemaGraph,
ty: &SchemaType,
json: &Value,
policy: JsonDecodePolicy,
) -> Result<SchemaValue, RenderError> {
let mut path = PathStack::new();
let mut visited: HashSet<TypeId> = HashSet::new();
from_json_inner(graph, ty, json, &mut path, &mut visited, policy)
}
struct ToJsonRenderer {
path: PathStack,
redact: bool,
}
impl SchemaWalker for ToJsonRenderer {
type Output = Value;
type Error = RenderError;
fn walk(
&mut self,
graph: &SchemaGraph,
ty: &SchemaType,
value: &SchemaValue,
) -> Result<Value, RenderError> {
encode(self, graph, ty, value)
}
}
fn drive<T>(res: Result<T, WalkerError<RenderError>>) -> Result<T, RenderError> {
match res {
Ok(v) => Ok(v),
Err(WalkerError::Walker(e)) => Err(e),
Err(WalkerError::RefCycle(id)) => Err(RenderError::ValueMismatch {
path: String::new(),
reason: format!("reference cycle through `{id}`"),
}),
Err(WalkerError::DanglingRef(id)) => Err(RenderError::ValueMismatch {
path: String::new(),
reason: format!("dangling reference `{id}`"),
}),
}
}
fn encode(
r: &mut ToJsonRenderer,
graph: &SchemaGraph,
ty: &SchemaType,
value: &SchemaValue,
) -> Result<Value, RenderError> {
if r.redact
&& let (Some(type_kind), Some(value_kind)) = (
HostManagedKind::from_type(ty),
HostManagedKind::from_value(value),
)
&& type_kind == value_kind
{
return Ok(Value::String(type_kind.redacted_placeholder().to_string()));
}
match (ty, value) {
(SchemaType::Ref { .. }, _) => unreachable!("walker resolves refs"),
(SchemaType::Bool { .. }, SchemaValue::Bool(b)) => Ok(Value::Bool(*b)),
(SchemaType::S8 { .. }, SchemaValue::S8(i)) => Ok(json_number_i64(*i as i64)),
(SchemaType::S16 { .. }, SchemaValue::S16(i)) => Ok(json_number_i64(*i as i64)),
(SchemaType::S32 { .. }, SchemaValue::S32(i)) => Ok(json_number_i64(*i as i64)),
(SchemaType::S64 { .. }, SchemaValue::S64(i)) => Ok(Value::String(i.to_string())),
(SchemaType::U8 { .. }, SchemaValue::U8(u)) => Ok(json_number_u64(*u as u64)),
(SchemaType::U16 { .. }, SchemaValue::U16(u)) => Ok(json_number_u64(*u as u64)),
(SchemaType::U32 { .. }, SchemaValue::U32(u)) => Ok(json_number_u64(*u as u64)),
(SchemaType::U64 { .. }, SchemaValue::U64(u)) => Ok(Value::String(u.to_string())),
(SchemaType::F32 { .. }, SchemaValue::F32(f)) => json_number_f64(*f as f64, &r.path),
(SchemaType::F64 { .. }, SchemaValue::F64(f)) => json_number_f64(*f, &r.path),
(SchemaType::Char { .. }, SchemaValue::Char(c)) => Ok(Value::String(c.to_string())),
(SchemaType::String { .. }, SchemaValue::String(s)) => Ok(Value::String(s.clone())),
(SchemaType::Text { .. }, SchemaValue::Text(p)) => Ok(canonical::text::to_json(p)),
(SchemaType::Binary { .. }, SchemaValue::Binary(p)) => {
canonical::binary::to_json(p).map_err(RenderError::from)
}
(SchemaType::Path { .. }, SchemaValue::Path { path }) => {
canonical::path::to_json(path).map_err(RenderError::from)
}
(SchemaType::Url { .. }, SchemaValue::Url { url }) => {
canonical::url::to_json(url).map_err(RenderError::from)
}
(SchemaType::Uuid { .. }, SchemaValue::Uuid(value)) => Ok(canonical::uuid::to_json(value)),
(SchemaType::Datetime { .. }, SchemaValue::Datetime { value }) => {
canonical::datetime::to_json(value).map_err(RenderError::from)
}
(SchemaType::Duration { .. }, SchemaValue::Duration(p)) => {
Ok(canonical::duration::to_json(p))
}
(SchemaType::Quantity { .. }, SchemaValue::Quantity(q)) => {
Ok(canonical::quantity::to_json(q))
}
#[cfg(not(all(feature = "guest", not(feature = "host"))))]
(SchemaType::Secret { .. }, SchemaValue::Secret(p)) => {
canonical::secret::to_json(p).map_err(RenderError::from)
}
#[cfg(not(all(feature = "guest", not(feature = "host"))))]
(SchemaType::QuotaToken { .. }, SchemaValue::QuotaToken(p)) => {
canonical::quota_token::to_json(p).map_err(RenderError::from)
}
#[cfg(not(all(feature = "guest", not(feature = "host"))))]
(SchemaType::PermissionCard { .. }, SchemaValue::PermissionCard(p)) => {
canonical::permission_card::to_json(p).map_err(RenderError::from)
}
#[cfg(all(feature = "guest", not(feature = "host")))]
(
SchemaType::Secret { .. }
| SchemaType::QuotaToken { .. }
| SchemaType::PermissionCard { .. },
SchemaValue::Secret(_) | SchemaValue::QuotaToken(_) | SchemaValue::PermissionCard(_),
) => Err(RenderError::Unsupported(
"opaque host-managed capabilities cannot be rendered by a guest",
)),
(SchemaType::Record { fields, .. }, SchemaValue::Record { fields: vs }) => {
if fields.len() != vs.len() {
return Err(r.mismatch(format!(
"record arity: expected {}, found {}",
fields.len(),
vs.len()
)));
}
let mut obj = Map::new();
for (field, value) in fields.iter().zip(vs.iter()) {
r.path.push(PathSegment::Field(field.name.clone()));
let rendered = drive(walk(r, graph, &field.body, value))?;
r.path.pop();
obj.insert(field.name.clone(), rendered);
}
Ok(Value::Object(obj))
}
(SchemaType::Variant { cases, .. }, SchemaValue::Variant(vp)) => {
let case_index = vp.case as usize;
if case_index >= cases.len() {
return Err(r.mismatch(format!(
"variant case index {} out of range (count {})",
vp.case,
cases.len()
)));
}
let case: &VariantCaseType = &cases[case_index];
match (&case.payload, &vp.payload) {
(None, None) => Ok(Value::String(case.name.clone())),
(Some(case_ty), Some(payload)) => {
r.path.push(PathSegment::Variant(case.name.clone()));
let rendered = drive(walk(r, graph, case_ty, payload))?;
r.path.pop();
let mut obj = Map::new();
obj.insert(case.name.clone(), rendered);
Ok(Value::Object(obj))
}
_ => Err(r.mismatch(format!(
"variant payload presence mismatch on case `{}`",
case.name
))),
}
}
(SchemaType::Enum { cases, .. }, SchemaValue::Enum { case }) => {
let idx = *case as usize;
if idx >= cases.len() {
return Err(r.mismatch(format!(
"enum case index {} out of range (count {})",
case,
cases.len()
)));
}
Ok(Value::String(cases[idx].clone()))
}
(SchemaType::Flags { flags, .. }, SchemaValue::Flags { bits }) => {
if flags.len() != bits.len() {
return Err(r.mismatch(format!(
"flags arity: expected {}, found {}",
flags.len(),
bits.len()
)));
}
let selected: Vec<Value> = flags
.iter()
.zip(bits.iter())
.filter(|(_, on)| **on)
.map(|(name, _)| Value::String(name.clone()))
.collect();
Ok(Value::Array(selected))
}
(SchemaType::Tuple { elements, .. }, SchemaValue::Tuple { elements: vs }) => {
if elements.len() != vs.len() {
return Err(r.mismatch(format!(
"tuple arity: expected {}, found {}",
elements.len(),
vs.len()
)));
}
let mut out = Vec::with_capacity(vs.len());
for (i, (et, ev)) in elements.iter().zip(vs.iter()).enumerate() {
r.path.push(PathSegment::Index(i));
let rendered = drive(walk(r, graph, et, ev))?;
r.path.pop();
out.push(rendered);
}
Ok(Value::Array(out))
}
(SchemaType::List { element, .. }, SchemaValue::List { elements }) => {
let mut out = Vec::with_capacity(elements.len());
for (i, ev) in elements.iter().enumerate() {
r.path.push(PathSegment::Index(i));
let rendered = drive(walk(r, graph, element, ev))?;
r.path.pop();
out.push(rendered);
}
Ok(Value::Array(out))
}
(
SchemaType::FixedList {
element, length, ..
},
SchemaValue::FixedList { elements },
) => {
if elements.len() as u32 != *length {
return Err(r.mismatch(format!(
"fixed list length: expected {}, found {}",
length,
elements.len()
)));
}
let mut out = Vec::with_capacity(elements.len());
for (i, ev) in elements.iter().enumerate() {
r.path.push(PathSegment::Index(i));
let rendered = drive(walk(r, graph, element, ev))?;
r.path.pop();
out.push(rendered);
}
Ok(Value::Array(out))
}
(SchemaType::Map { key, value, .. }, SchemaValue::Map { entries }) => {
let mut out = Vec::with_capacity(entries.len());
for (i, (k, v)) in entries.iter().enumerate() {
r.path.push(PathSegment::MapKey(i));
let rk = drive(walk(r, graph, key, k))?;
r.path.pop();
r.path.push(PathSegment::MapValue(i));
let rv = drive(walk(r, graph, value, v))?;
r.path.pop();
out.push(Value::Array(vec![rk, rv]));
}
Ok(Value::Array(out))
}
(SchemaType::Option { inner, .. }, SchemaValue::Option { inner: v }) => match v {
None => Ok(Value::Null),
Some(inner_value) => {
r.path.push(PathSegment::OptionInner);
let rendered = drive(walk(r, graph, inner, inner_value))?;
r.path.pop();
Ok(rendered)
}
},
(SchemaType::Result { spec, .. }, SchemaValue::Result(payload)) => {
encode_result(r, graph, spec, payload)
}
(SchemaType::Union { spec, .. }, SchemaValue::Union(payload)) => {
encode_union(r, graph, spec, payload)
}
(SchemaType::Future { .. }, _) | (SchemaType::Stream { .. }, _) => Err(
RenderError::Unsupported("future/stream values have no JSON representation"),
),
(ty, value) => Err(r.mismatch(format!(
"shape mismatch: expected {}, found {}",
type_name(ty),
value_name(value)
))),
}
}
fn encode_result(
r: &mut ToJsonRenderer,
graph: &SchemaGraph,
spec: &ResultSpec,
payload: &ResultValuePayload,
) -> Result<Value, RenderError> {
let mut obj = Map::new();
match payload {
ResultValuePayload::Ok {
value: payload_value,
} => {
let v = match (spec.ok.as_deref(), payload_value.as_deref()) {
(None, None) => Value::Null,
(Some(ok_ty), Some(inner)) => {
r.path.push(PathSegment::Ok);
let v = drive(walk(r, graph, ok_ty, inner))?;
r.path.pop();
v
}
_ => {
return Err(r.mismatch("result ok payload presence mismatch".to_string()));
}
};
obj.insert("ok".to_string(), v);
}
ResultValuePayload::Err {
value: payload_value,
} => {
let v = match (spec.err.as_deref(), payload_value.as_deref()) {
(None, None) => Value::Null,
(Some(err_ty), Some(inner)) => {
r.path.push(PathSegment::Err);
let v = drive(walk(r, graph, err_ty, inner))?;
r.path.pop();
v
}
_ => {
return Err(r.mismatch("result err payload presence mismatch".to_string()));
}
};
obj.insert("err".to_string(), v);
}
}
Ok(Value::Object(obj))
}
fn encode_union(
r: &mut ToJsonRenderer,
graph: &SchemaGraph,
spec: &UnionSpec,
payload: &UnionValuePayload,
) -> Result<Value, RenderError> {
let branch = find_branch(spec, &payload.tag)
.ok_or_else(|| r.mismatch(format!("unknown union branch tag `{}`", payload.tag)))?;
r.path.push(PathSegment::Union(payload.tag.clone()));
let rendered = drive(walk(r, graph, &branch.body, &payload.body))?;
r.path.pop();
if !rule_matches(&branch.discriminator, &rendered)? {
return Err(RenderError::UnionTagMismatch {
tag: payload.tag.clone(),
reason: format!(
"encoded body does not satisfy discriminator {}",
rule_label(&branch.discriminator)
),
});
}
Ok(rendered)
}
fn find_branch<'a>(spec: &'a UnionSpec, tag: &str) -> Option<&'a UnionBranch> {
spec.branches.iter().find(|b| b.tag == tag)
}
impl ToJsonRenderer {
fn mismatch(&self, reason: String) -> RenderError {
RenderError::ValueMismatch {
path: self.path.render(),
reason,
}
}
}
fn from_json_inner(
graph: &SchemaGraph,
ty: &SchemaType,
json: &Value,
path: &mut PathStack,
visited: &mut HashSet<TypeId>,
policy: JsonDecodePolicy,
) -> Result<SchemaValue, RenderError> {
let res = resolve_ref::<_, SchemaValue, RenderError>(graph, ty, visited, |graph, body| {
match from_json_body(graph, body, json, path, &mut HashSet::new(), policy) {
Ok(v) => Ok(v),
Err(e) => Err(WalkerError::Walker(e)),
}
});
drive_with_path(res, path)
}
fn drive_with_path(
res: Result<SchemaValue, WalkerError<RenderError>>,
path: &PathStack,
) -> Result<SchemaValue, RenderError> {
match res {
Ok(v) => Ok(v),
Err(WalkerError::Walker(e)) => Err(e),
Err(WalkerError::RefCycle(id)) => Err(RenderError::ValueMismatch {
path: path.render(),
reason: format!("reference cycle through `{id}`"),
}),
Err(WalkerError::DanglingRef(id)) => Err(RenderError::ValueMismatch {
path: path.render(),
reason: format!("dangling reference `{id}`"),
}),
}
}
fn from_json_body(
graph: &SchemaGraph,
ty: &SchemaType,
json: &Value,
path: &mut PathStack,
_local_visited: &mut HashSet<TypeId>,
policy: JsonDecodePolicy,
) -> Result<SchemaValue, RenderError> {
let mut visited: HashSet<TypeId> = HashSet::new();
if policy == JsonDecodePolicy::Untrusted
&& let Some(kind) = HostManagedKind::from_type(ty)
{
return Err(mismatch(
path,
format!(
"host-managed capability `{}` cannot be constructed from external JSON",
kind.kind_name()
),
));
}
match ty {
SchemaType::Ref { .. } => unreachable!("ref resolved by resolve_ref"),
SchemaType::Bool { .. } => match json.as_bool() {
Some(b) => Ok(SchemaValue::Bool(b)),
None => Err(mismatch(path, "expected JSON boolean".to_string())),
},
SchemaType::S8 { .. } => {
check_int_range::<i8>(json, path)?;
Ok(SchemaValue::S8(json_i64(json, path)? as i8))
}
SchemaType::S16 { .. } => {
check_int_range::<i16>(json, path)?;
Ok(SchemaValue::S16(json_i64(json, path)? as i16))
}
SchemaType::S32 { .. } => {
check_int_range::<i32>(json, path)?;
Ok(SchemaValue::S32(json_i64(json, path)? as i32))
}
SchemaType::S64 { .. } => Ok(SchemaValue::S64(json_i64_string(json, path)?)),
SchemaType::U8 { .. } => {
check_int_range::<u8>(json, path)?;
Ok(SchemaValue::U8(json_u64(json, path)? as u8))
}
SchemaType::U16 { .. } => {
check_int_range::<u16>(json, path)?;
Ok(SchemaValue::U16(json_u64(json, path)? as u16))
}
SchemaType::U32 { .. } => {
check_int_range::<u32>(json, path)?;
Ok(SchemaValue::U32(json_u64(json, path)? as u32))
}
SchemaType::U64 { .. } => Ok(SchemaValue::U64(json_u64_string(json, path)?)),
SchemaType::F32 { .. } => {
let value = json_f64(json, path)?;
check_f32_in_range(value, path)?;
Ok(SchemaValue::F32(value as f32))
}
SchemaType::F64 { .. } => {
let value = json_f64(json, path)?;
if !value.is_finite() {
return Err(mismatch(path, "f64 must be finite".to_string()));
}
Ok(SchemaValue::F64(value))
}
SchemaType::Char { .. } => match json.as_str() {
Some(s) => {
let mut chars = s.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => Ok(SchemaValue::Char(c)),
_ => Err(mismatch(
path,
"expected JSON string with exactly one char".to_string(),
)),
}
}
None => Err(mismatch(path, "expected JSON string".to_string())),
},
SchemaType::String { .. } => match json.as_str() {
Some(s) => Ok(SchemaValue::String(s.to_string())),
None => Err(mismatch(path, "expected JSON string".to_string())),
},
SchemaType::Text { .. } => {
let p = canonical::text::from_json(json)?;
Ok(SchemaValue::Text(p))
}
SchemaType::Binary { .. } => {
let p = canonical::binary::from_json(json)?;
Ok(SchemaValue::Binary(p))
}
SchemaType::Path { .. } => {
let s = canonical::path::from_json(json)?;
Ok(SchemaValue::Path { path: s })
}
SchemaType::Url { .. } => {
let s = canonical::url::from_json(json)?;
Ok(SchemaValue::Url { url: s })
}
SchemaType::Uuid { .. } => {
let value = canonical::uuid::from_json(json)?;
Ok(SchemaValue::Uuid(value))
}
SchemaType::Datetime { .. } => {
let dt = canonical::datetime::from_json(json)?;
Ok(SchemaValue::Datetime { value: dt })
}
SchemaType::Duration { .. } => {
let p = canonical::duration::from_json(json)?;
Ok(SchemaValue::Duration(p))
}
SchemaType::Quantity { .. } => {
let q = canonical::quantity::from_json(json)?;
Ok(SchemaValue::Quantity(q))
}
#[cfg(not(all(feature = "guest", not(feature = "host"))))]
SchemaType::Secret { .. } => {
let p = canonical::secret::from_json(json)?;
Ok(SchemaValue::Secret(p))
}
#[cfg(not(all(feature = "guest", not(feature = "host"))))]
SchemaType::QuotaToken { .. } => {
let p = canonical::quota_token::from_json(json)?;
Ok(SchemaValue::QuotaToken(p))
}
#[cfg(not(all(feature = "guest", not(feature = "host"))))]
SchemaType::PermissionCard { .. } => {
let p = canonical::permission_card::from_json(json)?;
Ok(SchemaValue::PermissionCard(p))
}
#[cfg(all(feature = "guest", not(feature = "host")))]
SchemaType::Secret { .. }
| SchemaType::QuotaToken { .. }
| SchemaType::PermissionCard { .. } => Err(RenderError::Unsupported(
"opaque host-managed capabilities cannot be constructed by a guest",
)),
SchemaType::Record { fields, .. } => {
let obj = json
.as_object()
.ok_or_else(|| mismatch(path, "expected JSON object for record".to_string()))?;
let known: HashSet<&str> = fields.iter().map(|f| f.name.as_str()).collect();
for key in obj.keys() {
if !known.contains(key.as_str()) {
return Err(RenderError::UnexpectedField {
record: "anonymous".to_string(),
field: key.clone(),
});
}
}
let mut out = Vec::with_capacity(fields.len());
for field in fields.iter() {
match obj.get(&field.name) {
Some(value) => {
path.push(PathSegment::Field(field.name.clone()));
let v =
from_json_inner(graph, &field.body, value, path, &mut visited, policy)?;
path.pop();
out.push(v);
}
None if resolves_to_option(graph, &field.body) => {
out.push(SchemaValue::Option { inner: None });
}
None => {
return Err(mismatch(
path,
format!("missing record field `{}`", field.name),
));
}
}
}
Ok(SchemaValue::Record { fields: out })
}
SchemaType::Variant { cases, .. } => match json {
Value::String(name) => {
let (idx, case) = find_variant_case(cases, name)
.ok_or_else(|| mismatch(path, format!("unknown variant case `{name}`")))?;
if case.payload.is_some() {
return Err(mismatch(
path,
format!("variant case `{name}` requires a payload"),
));
}
Ok(SchemaValue::Variant(VariantValuePayload {
case: idx as u32,
payload: None,
}))
}
Value::Object(obj) if obj.len() == 1 => {
let (name, payload_json) = obj.iter().next().unwrap();
let (idx, case) = find_variant_case(cases, name)
.ok_or_else(|| mismatch(path, format!("unknown variant case `{name}`")))?;
let payload_ty = case.payload.as_ref().ok_or_else(|| {
mismatch(
path,
format!("variant case `{name}` does not accept a payload"),
)
})?;
path.push(PathSegment::Variant(name.clone()));
let inner =
from_json_inner(graph, payload_ty, payload_json, path, &mut visited, policy)?;
path.pop();
Ok(SchemaValue::Variant(VariantValuePayload {
case: idx as u32,
payload: Some(Box::new(inner)),
}))
}
_ => Err(mismatch(
path,
"expected JSON string or single-entry object for variant".to_string(),
)),
},
SchemaType::Enum { cases, .. } => {
let name = json
.as_str()
.ok_or_else(|| mismatch(path, "expected JSON string for enum".to_string()))?;
let idx = cases
.iter()
.position(|c| c == name)
.ok_or_else(|| mismatch(path, format!("unknown enum case `{name}`")))?;
Ok(SchemaValue::Enum { case: idx as u32 })
}
SchemaType::Flags { flags, .. } => {
let arr = json
.as_array()
.ok_or_else(|| mismatch(path, "expected JSON array for flags".to_string()))?;
let mut selected: HashSet<&str> = HashSet::new();
for item in arr {
let name = item.as_str().ok_or_else(|| {
mismatch(path, "expected JSON string in flags array".to_string())
})?;
if !flags.iter().any(|f| f == name) {
return Err(mismatch(path, format!("unknown flag `{name}`")));
}
if !selected.insert(name) {
return Err(RenderError::DuplicateFlag {
flag: name.to_string(),
});
}
}
let bits: Vec<bool> = flags
.iter()
.map(|f| selected.contains(f.as_str()))
.collect();
Ok(SchemaValue::Flags { bits })
}
SchemaType::Tuple { elements, .. } => {
let arr = json
.as_array()
.ok_or_else(|| mismatch(path, "expected JSON array for tuple".to_string()))?;
if arr.len() != elements.len() {
return Err(mismatch(
path,
format!(
"tuple arity: expected {}, found {}",
elements.len(),
arr.len()
),
));
}
let mut out = Vec::with_capacity(arr.len());
for (i, (et, ev)) in elements.iter().zip(arr.iter()).enumerate() {
path.push(PathSegment::Index(i));
let v = from_json_inner(graph, et, ev, path, &mut visited, policy)?;
path.pop();
out.push(v);
}
Ok(SchemaValue::Tuple { elements: out })
}
SchemaType::List { element, .. } => {
let arr = json
.as_array()
.ok_or_else(|| mismatch(path, "expected JSON array for list".to_string()))?;
let mut out = Vec::with_capacity(arr.len());
for (i, ev) in arr.iter().enumerate() {
path.push(PathSegment::Index(i));
let v = from_json_inner(graph, element, ev, path, &mut visited, policy)?;
path.pop();
out.push(v);
}
Ok(SchemaValue::List { elements: out })
}
SchemaType::FixedList {
element, length, ..
} => {
let arr = json
.as_array()
.ok_or_else(|| mismatch(path, "expected JSON array for fixed list".to_string()))?;
if arr.len() as u32 != *length {
return Err(mismatch(
path,
format!(
"fixed list length: expected {}, found {}",
length,
arr.len()
),
));
}
let mut out = Vec::with_capacity(arr.len());
for (i, ev) in arr.iter().enumerate() {
path.push(PathSegment::Index(i));
let v = from_json_inner(graph, element, ev, path, &mut visited, policy)?;
path.pop();
out.push(v);
}
Ok(SchemaValue::FixedList { elements: out })
}
SchemaType::Map { key, value, .. } => {
let arr = json
.as_array()
.ok_or_else(|| mismatch(path, "expected JSON array for map".to_string()))?;
let mut out: Vec<(SchemaValue, SchemaValue)> = Vec::with_capacity(arr.len());
for (i, item) in arr.iter().enumerate() {
let pair = item.as_array().ok_or_else(|| {
mismatch(path, "expected `[k, v]` array for map entry".to_string())
})?;
if pair.len() != 2 {
return Err(mismatch(
path,
format!(
"map entry must have exactly 2 elements, found {}",
pair.len()
),
));
}
path.push(PathSegment::MapKey(i));
let k = from_json_inner(graph, key, &pair[0], path, &mut visited, policy)?;
path.pop();
path.push(PathSegment::MapValue(i));
let v = from_json_inner(graph, value, &pair[1], path, &mut visited, policy)?;
path.pop();
out.push((k, v));
}
Ok(SchemaValue::Map { entries: out })
}
SchemaType::Option { inner, .. } => match json {
Value::Null => Ok(SchemaValue::Option { inner: None }),
other => {
path.push(PathSegment::OptionInner);
let v = from_json_inner(graph, inner, other, path, &mut visited, policy)?;
path.pop();
Ok(SchemaValue::Option {
inner: Some(Box::new(v)),
})
}
},
SchemaType::Result { spec, .. } => {
decode_result(graph, spec, json, path, &mut visited, policy)
}
SchemaType::Union { spec, .. } => decode_union(graph, spec, json, path, policy),
SchemaType::Future { .. } | SchemaType::Stream { .. } => Err(RenderError::Unsupported(
"future/stream values have no JSON representation",
)),
}
}
fn resolves_to_option(graph: &SchemaGraph, ty: &SchemaType) -> bool {
let mut current = ty;
let mut visited = HashSet::new();
loop {
match current {
SchemaType::Option { .. } => return true,
SchemaType::Ref { id, .. } if visited.insert(id.clone()) => match graph.lookup(id) {
Some(definition) => current = &definition.body,
None => return false,
},
_ => return false,
}
}
}
fn decode_result(
graph: &SchemaGraph,
spec: &ResultSpec,
json: &Value,
path: &mut PathStack,
visited: &mut HashSet<TypeId>,
policy: JsonDecodePolicy,
) -> Result<SchemaValue, RenderError> {
let obj = json
.as_object()
.ok_or_else(|| mismatch(path, "expected JSON object for result".to_string()))?;
if obj.len() != 1 {
return Err(mismatch(
path,
"result object must have exactly one key `ok` or `err`".to_string(),
));
}
let (key, payload_json) = obj.iter().next().unwrap();
match key.as_str() {
"ok" => {
let value = match (spec.ok.as_deref(), payload_json) {
(None, Value::Null) => None,
(Some(ok_ty), other) => {
path.push(PathSegment::Ok);
let v = from_json_inner(graph, ok_ty, other, path, visited, policy)?;
path.pop();
Some(Box::new(v))
}
_ => {
return Err(mismatch(
path,
"result ok payload presence mismatch".to_string(),
));
}
};
Ok(SchemaValue::Result(ResultValuePayload::Ok { value }))
}
"err" => {
let value = match (spec.err.as_deref(), payload_json) {
(None, Value::Null) => None,
(Some(err_ty), other) => {
path.push(PathSegment::Err);
let v = from_json_inner(graph, err_ty, other, path, visited, policy)?;
path.pop();
Some(Box::new(v))
}
_ => {
return Err(mismatch(
path,
"result err payload presence mismatch".to_string(),
));
}
};
Ok(SchemaValue::Result(ResultValuePayload::Err { value }))
}
other => Err(mismatch(path, format!("unexpected result key `{other}`"))),
}
}
fn decode_union(
graph: &SchemaGraph,
spec: &UnionSpec,
json: &Value,
path: &mut PathStack,
policy: JsonDecodePolicy,
) -> Result<SchemaValue, RenderError> {
let mut matched: Vec<&UnionBranch> = Vec::new();
for branch in spec.branches.iter() {
if rule_matches(&branch.discriminator, json)? {
matched.push(branch);
}
}
match matched.as_slice() {
[] => Err(RenderError::UnionNoMatch),
[branch] => {
path.push(PathSegment::Union(branch.tag.clone()));
let body =
from_json_inner(graph, &branch.body, json, path, &mut HashSet::new(), policy)?;
path.pop();
Ok(SchemaValue::Union(UnionValuePayload {
tag: branch.tag.clone(),
body: Box::new(body),
}))
}
multiple => Err(RenderError::UnionAmbiguous {
matched: multiple.iter().map(|b| b.tag.clone()).collect(),
}),
}
}
fn rule_matches(rule: &DiscriminatorRule, json: &Value) -> Result<bool, RenderError> {
Ok(match rule {
DiscriminatorRule::Prefix { prefix } => json
.as_str()
.map(|s| s.starts_with(prefix.as_str()))
.unwrap_or(false),
DiscriminatorRule::Suffix { suffix } => json
.as_str()
.map(|s| s.ends_with(suffix.as_str()))
.unwrap_or(false),
DiscriminatorRule::Contains { substring } => json
.as_str()
.map(|s| s.contains(substring.as_str()))
.unwrap_or(false),
#[cfg(not(feature = "regex"))]
DiscriminatorRule::Regex { .. } => return Err(RenderError::Unsupported("feature `regex`")),
#[cfg(feature = "regex")]
DiscriminatorRule::Regex { regex } => match (json.as_str(), regex::Regex::new(regex)) {
(Some(s), Ok(re)) => re.is_match(s),
_ => false,
},
DiscriminatorRule::FieldEquals(disc) => json
.as_object()
.and_then(|obj| obj.get(disc.field_name.as_str()))
.map(|v| match &disc.literal {
None => true,
Some(lit) => v.as_str().map(|s| s == lit.as_str()).unwrap_or(false),
})
.unwrap_or(false),
DiscriminatorRule::FieldAbsent { field_name } => json
.as_object()
.map(|obj| !obj.contains_key(field_name.as_str()))
.unwrap_or(false),
})
}
fn rule_label(rule: &DiscriminatorRule) -> String {
match rule {
DiscriminatorRule::Prefix { prefix } => format!("prefix `{prefix}`"),
DiscriminatorRule::Suffix { suffix } => format!("suffix `{suffix}`"),
DiscriminatorRule::Contains { substring } => format!("contains `{substring}`"),
DiscriminatorRule::Regex { regex } => format!("regex `{regex}`"),
DiscriminatorRule::FieldEquals(disc) => match &disc.literal {
None => format!("field `{}` present", disc.field_name),
Some(lit) => format!("field `{}` == `{lit}`", disc.field_name),
},
DiscriminatorRule::FieldAbsent { field_name } => format!("field `{field_name}` absent"),
}
}
fn find_variant_case<'a>(
cases: &'a [VariantCaseType],
name: &str,
) -> Option<(usize, &'a VariantCaseType)> {
cases.iter().enumerate().find(|(_, c)| c.name == name)
}
fn json_i64(json: &Value, path: &mut PathStack) -> Result<i64, RenderError> {
json.as_i64()
.ok_or_else(|| mismatch(path, "expected JSON integer".to_string()))
}
fn json_u64(json: &Value, path: &mut PathStack) -> Result<u64, RenderError> {
json.as_u64()
.ok_or_else(|| mismatch(path, "expected JSON non-negative integer".to_string()))
}
fn json_i64_string(json: &Value, path: &mut PathStack) -> Result<i64, RenderError> {
let value = json
.as_str()
.ok_or_else(|| mismatch(path, "expected canonical signed integer string".to_string()))?;
if !is_canonical_signed_integer(value) {
return Err(mismatch(
path,
"expected canonical signed integer string".to_string(),
));
}
value
.parse()
.map_err(|_| mismatch(path, "signed 64-bit integer out of range".to_string()))
}
fn json_u64_string(json: &Value, path: &mut PathStack) -> Result<u64, RenderError> {
let value = json.as_str().ok_or_else(|| {
mismatch(
path,
"expected canonical unsigned integer string".to_string(),
)
})?;
if value != "0" && !canonical_nonzero_digits(value) {
return Err(mismatch(
path,
"expected canonical unsigned integer string".to_string(),
));
}
value
.parse()
.map_err(|_| mismatch(path, "unsigned 64-bit integer out of range".to_string()))
}
fn is_canonical_signed_integer(value: &str) -> bool {
value == "0"
|| value
.strip_prefix('-')
.is_some_and(canonical_nonzero_digits)
|| canonical_nonzero_digits(value)
}
fn canonical_nonzero_digits(value: &str) -> bool {
value
.as_bytes()
.first()
.is_some_and(|digit| matches!(digit, b'1'..=b'9'))
&& value.bytes().all(|digit| digit.is_ascii_digit())
}
fn json_f64(json: &Value, path: &mut PathStack) -> Result<f64, RenderError> {
json.as_f64()
.ok_or_else(|| mismatch(path, "expected JSON number".to_string()))
}
fn json_number_i64(value: i64) -> Value {
Value::Number(Number::from(value))
}
fn json_number_u64(value: u64) -> Value {
Value::Number(Number::from(value))
}
fn json_number_f64(value: f64, path: &PathStack) -> Result<Value, RenderError> {
Number::from_f64(value)
.map(Value::Number)
.ok_or_else(|| RenderError::Json(format!("non-finite float at {}", path.render())))
}
fn check_f32_in_range(value: f64, path: &mut PathStack) -> Result<(), RenderError> {
if !value.is_finite() {
return Err(mismatch(path, "f32 must be finite".to_string()));
}
if value.abs() > f32::MAX as f64 {
return Err(mismatch(path, "f32 out of range".to_string()));
}
Ok(())
}
fn check_int_range<T: TryFrom<i128>>(
json: &Value,
path: &mut PathStack,
) -> Result<(), RenderError> {
let n = json
.as_i64()
.map(i128::from)
.or_else(|| json.as_u64().map(i128::from))
.ok_or_else(|| mismatch(path, "expected JSON integer".to_string()))?;
T::try_from(n)
.map(|_| ())
.map_err(|_| mismatch(path, "integer out of range".to_string()))
}
fn mismatch(path: &PathStack, reason: String) -> RenderError {
RenderError::ValueMismatch {
path: path.render(),
reason,
}
}
fn type_name(ty: &SchemaType) -> &'static str {
match ty {
SchemaType::Ref { .. } => "ref",
SchemaType::Bool { .. } => "bool",
SchemaType::S8 { .. } => "s8",
SchemaType::S16 { .. } => "s16",
SchemaType::S32 { .. } => "s32",
SchemaType::S64 { .. } => "s64",
SchemaType::U8 { .. } => "u8",
SchemaType::U16 { .. } => "u16",
SchemaType::U32 { .. } => "u32",
SchemaType::U64 { .. } => "u64",
SchemaType::F32 { .. } => "f32",
SchemaType::F64 { .. } => "f64",
SchemaType::Char { .. } => "char",
SchemaType::String { .. } => "string",
SchemaType::Record { .. } => "record",
SchemaType::Variant { .. } => "variant",
SchemaType::Enum { .. } => "enum",
SchemaType::Flags { .. } => "flags",
SchemaType::Tuple { .. } => "tuple",
SchemaType::List { .. } => "list",
SchemaType::FixedList { .. } => "fixed-list",
SchemaType::Map { .. } => "map",
SchemaType::Option { .. } => "option",
SchemaType::Result { .. } => "result",
SchemaType::Text { .. } => "text",
SchemaType::Binary { .. } => "binary",
SchemaType::Path { .. } => "path",
SchemaType::Url { .. } => "url",
SchemaType::Uuid { .. } => "uuid",
SchemaType::Datetime { .. } => "datetime",
SchemaType::Duration { .. } => "duration",
SchemaType::Quantity { .. } => "quantity",
SchemaType::Union { .. } => "union",
SchemaType::Secret { .. } => "secret",
SchemaType::QuotaToken { .. } => "quota-token",
SchemaType::PermissionCard { .. } => "permission-card",
SchemaType::Future { .. } => "future",
SchemaType::Stream { .. } => "stream",
}
}
fn value_name(value: &SchemaValue) -> &'static str {
match value {
SchemaValue::Bool(_) => "bool",
SchemaValue::S8(_) => "s8",
SchemaValue::S16(_) => "s16",
SchemaValue::S32(_) => "s32",
SchemaValue::S64(_) => "s64",
SchemaValue::U8(_) => "u8",
SchemaValue::U16(_) => "u16",
SchemaValue::U32(_) => "u32",
SchemaValue::U64(_) => "u64",
SchemaValue::F32(_) => "f32",
SchemaValue::F64(_) => "f64",
SchemaValue::Char(_) => "char",
SchemaValue::String(_) => "string",
SchemaValue::Record { .. } => "record",
SchemaValue::Variant(_) => "variant",
SchemaValue::Enum { .. } => "enum",
SchemaValue::Flags { .. } => "flags",
SchemaValue::Tuple { .. } => "tuple",
SchemaValue::List { .. } => "list",
SchemaValue::FixedList { .. } => "fixed-list",
SchemaValue::Map { .. } => "map",
SchemaValue::Option { .. } => "option",
SchemaValue::Result(_) => "result",
SchemaValue::Text(_) => "text",
SchemaValue::Binary(_) => "binary",
SchemaValue::Path { .. } => "path",
SchemaValue::Url { .. } => "url",
SchemaValue::Uuid(_) => "uuid",
SchemaValue::Datetime { .. } => "datetime",
SchemaValue::Duration(_) => "duration",
SchemaValue::Quantity(_) => "quantity",
SchemaValue::Union(_) => "union",
SchemaValue::Secret(_) => "secret",
SchemaValue::QuotaToken(_) => "quota-token",
SchemaValue::Stream(_) => "stream",
SchemaValue::PermissionCard(_) => "permission-card",
}
}
#[derive(Clone, Debug)]
enum PathSegment {
Field(String),
Variant(String),
Index(usize),
OptionInner,
Ok,
Err,
Union(String),
MapKey(usize),
MapValue(usize),
}
struct PathStack {
segments: Vec<PathSegment>,
}
impl PathStack {
fn new() -> Self {
Self {
segments: Vec::new(),
}
}
fn push(&mut self, segment: PathSegment) {
self.segments.push(segment);
}
fn pop(&mut self) {
self.segments.pop();
}
fn render(&self) -> String {
let mut s = String::new();
for seg in &self.segments {
match seg {
PathSegment::Field(n) => {
s.push('.');
s.push_str(n);
}
PathSegment::Variant(n) => {
s.push('.');
s.push_str(n);
}
PathSegment::Index(i) => {
s.push('[');
s.push_str(&i.to_string());
s.push(']');
}
PathSegment::OptionInner => s.push_str(".some"),
PathSegment::Ok => s.push_str(".ok"),
PathSegment::Err => s.push_str(".err"),
PathSegment::Union(n) => {
s.push('.');
s.push_str(n);
}
PathSegment::MapKey(i) => {
s.push_str(".key[");
s.push_str(&i.to_string());
s.push(']');
}
PathSegment::MapValue(i) => {
s.push_str(".value[");
s.push_str(&i.to_string());
s.push(']');
}
}
}
if s.is_empty() { "$".to_string() } else { s }
}
}