1use crate::schema::canonical;
22use crate::schema::graph::SchemaGraph;
23use crate::schema::host_managed::HostManagedKind;
24use crate::schema::metadata::TypeId;
25use crate::schema::render::error::RenderError;
26use crate::schema::render::walker::{SchemaWalker, WalkerError, resolve_ref, walk};
27use crate::schema::schema_type::{
28 DiscriminatorRule, ResultSpec, SchemaType, UnionBranch, UnionSpec, VariantCaseType,
29};
30use crate::schema::schema_value::{
31 ResultValuePayload, SchemaValue, UnionValuePayload, VariantValuePayload,
32};
33use serde_json::{Map, Number, Value};
34use std::collections::HashSet;
35
36pub fn to_json_value(
38 graph: &SchemaGraph,
39 ty: &SchemaType,
40 value: &SchemaValue,
41) -> Result<Value, RenderError> {
42 let mut renderer = ToJsonRenderer {
43 path: PathStack::new(),
44 redact: false,
45 };
46 drive(walk(&mut renderer, graph, ty, value))
47}
48
49pub fn to_json_value_redacted(
66 graph: &SchemaGraph,
67 ty: &SchemaType,
68 value: &SchemaValue,
69) -> Result<Value, RenderError> {
70 let mut renderer = ToJsonRenderer {
71 path: PathStack::new(),
72 redact: true,
73 };
74 drive(walk(&mut renderer, graph, ty, value))
75}
76
77pub fn from_json_value(
79 graph: &SchemaGraph,
80 ty: &SchemaType,
81 json: &Value,
82) -> Result<SchemaValue, RenderError> {
83 from_json_value_with_policy(graph, ty, json, JsonDecodePolicy::Trusted)
84}
85
86pub fn from_untrusted_json_value(
91 graph: &SchemaGraph,
92 ty: &SchemaType,
93 json: &Value,
94) -> Result<SchemaValue, RenderError> {
95 from_json_value_with_policy(graph, ty, json, JsonDecodePolicy::Untrusted)
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99enum JsonDecodePolicy {
100 Trusted,
101 Untrusted,
102}
103
104fn from_json_value_with_policy(
105 graph: &SchemaGraph,
106 ty: &SchemaType,
107 json: &Value,
108 policy: JsonDecodePolicy,
109) -> Result<SchemaValue, RenderError> {
110 let mut path = PathStack::new();
111 let mut visited: HashSet<TypeId> = HashSet::new();
112 from_json_inner(graph, ty, json, &mut path, &mut visited, policy)
113}
114
115struct ToJsonRenderer {
118 path: PathStack,
119 redact: bool,
120}
121
122impl SchemaWalker for ToJsonRenderer {
123 type Output = Value;
124 type Error = RenderError;
125
126 fn walk(
127 &mut self,
128 graph: &SchemaGraph,
129 ty: &SchemaType,
130 value: &SchemaValue,
131 ) -> Result<Value, RenderError> {
132 encode(self, graph, ty, value)
133 }
134}
135
136fn drive<T>(res: Result<T, WalkerError<RenderError>>) -> Result<T, RenderError> {
137 match res {
138 Ok(v) => Ok(v),
139 Err(WalkerError::Walker(e)) => Err(e),
140 Err(WalkerError::RefCycle(id)) => Err(RenderError::ValueMismatch {
141 path: String::new(),
142 reason: format!("reference cycle through `{id}`"),
143 }),
144 Err(WalkerError::DanglingRef(id)) => Err(RenderError::ValueMismatch {
145 path: String::new(),
146 reason: format!("dangling reference `{id}`"),
147 }),
148 }
149}
150
151fn encode(
152 r: &mut ToJsonRenderer,
153 graph: &SchemaGraph,
154 ty: &SchemaType,
155 value: &SchemaValue,
156) -> Result<Value, RenderError> {
157 if r.redact
158 && let (Some(type_kind), Some(value_kind)) = (
159 HostManagedKind::from_type(ty),
160 HostManagedKind::from_value(value),
161 )
162 && type_kind == value_kind
163 {
164 return Ok(Value::String(type_kind.redacted_placeholder().to_string()));
165 }
166
167 match (ty, value) {
168 (SchemaType::Ref { .. }, _) => unreachable!("walker resolves refs"),
169
170 (SchemaType::Bool { .. }, SchemaValue::Bool(b)) => Ok(Value::Bool(*b)),
171 (SchemaType::S8 { .. }, SchemaValue::S8(i)) => Ok(json_number_i64(*i as i64)),
172 (SchemaType::S16 { .. }, SchemaValue::S16(i)) => Ok(json_number_i64(*i as i64)),
173 (SchemaType::S32 { .. }, SchemaValue::S32(i)) => Ok(json_number_i64(*i as i64)),
174 (SchemaType::S64 { .. }, SchemaValue::S64(i)) => Ok(Value::String(i.to_string())),
175 (SchemaType::U8 { .. }, SchemaValue::U8(u)) => Ok(json_number_u64(*u as u64)),
176 (SchemaType::U16 { .. }, SchemaValue::U16(u)) => Ok(json_number_u64(*u as u64)),
177 (SchemaType::U32 { .. }, SchemaValue::U32(u)) => Ok(json_number_u64(*u as u64)),
178 (SchemaType::U64 { .. }, SchemaValue::U64(u)) => Ok(Value::String(u.to_string())),
179 (SchemaType::F32 { .. }, SchemaValue::F32(f)) => json_number_f64(*f as f64, &r.path),
180 (SchemaType::F64 { .. }, SchemaValue::F64(f)) => json_number_f64(*f, &r.path),
181 (SchemaType::Char { .. }, SchemaValue::Char(c)) => Ok(Value::String(c.to_string())),
182 (SchemaType::String { .. }, SchemaValue::String(s)) => Ok(Value::String(s.clone())),
183
184 (SchemaType::Text { .. }, SchemaValue::Text(p)) => Ok(canonical::text::to_json(p)),
185 (SchemaType::Binary { .. }, SchemaValue::Binary(p)) => {
186 canonical::binary::to_json(p).map_err(RenderError::from)
187 }
188 (SchemaType::Path { .. }, SchemaValue::Path { path }) => {
189 canonical::path::to_json(path).map_err(RenderError::from)
190 }
191 (SchemaType::Url { .. }, SchemaValue::Url { url }) => {
192 canonical::url::to_json(url).map_err(RenderError::from)
193 }
194 (SchemaType::Uuid { .. }, SchemaValue::Uuid(value)) => Ok(canonical::uuid::to_json(value)),
195 (SchemaType::Datetime { .. }, SchemaValue::Datetime { value }) => {
196 canonical::datetime::to_json(value).map_err(RenderError::from)
197 }
198 (SchemaType::Duration { .. }, SchemaValue::Duration(p)) => {
199 Ok(canonical::duration::to_json(p))
200 }
201 (SchemaType::Quantity { .. }, SchemaValue::Quantity(q)) => {
202 Ok(canonical::quantity::to_json(q))
203 }
204 #[cfg(not(all(feature = "guest", not(feature = "host"))))]
205 (SchemaType::Secret { .. }, SchemaValue::Secret(p)) => {
206 canonical::secret::to_json(p).map_err(RenderError::from)
207 }
208 #[cfg(not(all(feature = "guest", not(feature = "host"))))]
209 (SchemaType::QuotaToken { .. }, SchemaValue::QuotaToken(p)) => {
210 canonical::quota_token::to_json(p).map_err(RenderError::from)
211 }
212 #[cfg(not(all(feature = "guest", not(feature = "host"))))]
213 (SchemaType::PermissionCard { .. }, SchemaValue::PermissionCard(p)) => {
214 canonical::permission_card::to_json(p).map_err(RenderError::from)
215 }
216 #[cfg(all(feature = "guest", not(feature = "host")))]
217 (
218 SchemaType::Secret { .. }
219 | SchemaType::QuotaToken { .. }
220 | SchemaType::PermissionCard { .. },
221 SchemaValue::Secret(_) | SchemaValue::QuotaToken(_) | SchemaValue::PermissionCard(_),
222 ) => Err(RenderError::Unsupported(
223 "opaque host-managed capabilities cannot be rendered by a guest",
224 )),
225
226 (SchemaType::Record { fields, .. }, SchemaValue::Record { fields: vs }) => {
227 if fields.len() != vs.len() {
228 return Err(r.mismatch(format!(
229 "record arity: expected {}, found {}",
230 fields.len(),
231 vs.len()
232 )));
233 }
234 let mut obj = Map::new();
235 for (field, value) in fields.iter().zip(vs.iter()) {
236 r.path.push(PathSegment::Field(field.name.clone()));
237 let rendered = drive(walk(r, graph, &field.body, value))?;
238 r.path.pop();
239 obj.insert(field.name.clone(), rendered);
240 }
241 Ok(Value::Object(obj))
242 }
243
244 (SchemaType::Variant { cases, .. }, SchemaValue::Variant(vp)) => {
245 let case_index = vp.case as usize;
246 if case_index >= cases.len() {
247 return Err(r.mismatch(format!(
248 "variant case index {} out of range (count {})",
249 vp.case,
250 cases.len()
251 )));
252 }
253 let case: &VariantCaseType = &cases[case_index];
254 match (&case.payload, &vp.payload) {
255 (None, None) => Ok(Value::String(case.name.clone())),
256 (Some(case_ty), Some(payload)) => {
257 r.path.push(PathSegment::Variant(case.name.clone()));
258 let rendered = drive(walk(r, graph, case_ty, payload))?;
259 r.path.pop();
260 let mut obj = Map::new();
261 obj.insert(case.name.clone(), rendered);
262 Ok(Value::Object(obj))
263 }
264 _ => Err(r.mismatch(format!(
265 "variant payload presence mismatch on case `{}`",
266 case.name
267 ))),
268 }
269 }
270
271 (SchemaType::Enum { cases, .. }, SchemaValue::Enum { case }) => {
272 let idx = *case as usize;
273 if idx >= cases.len() {
274 return Err(r.mismatch(format!(
275 "enum case index {} out of range (count {})",
276 case,
277 cases.len()
278 )));
279 }
280 Ok(Value::String(cases[idx].clone()))
281 }
282
283 (SchemaType::Flags { flags, .. }, SchemaValue::Flags { bits }) => {
284 if flags.len() != bits.len() {
285 return Err(r.mismatch(format!(
286 "flags arity: expected {}, found {}",
287 flags.len(),
288 bits.len()
289 )));
290 }
291 let selected: Vec<Value> = flags
292 .iter()
293 .zip(bits.iter())
294 .filter(|(_, on)| **on)
295 .map(|(name, _)| Value::String(name.clone()))
296 .collect();
297 Ok(Value::Array(selected))
298 }
299
300 (SchemaType::Tuple { elements, .. }, SchemaValue::Tuple { elements: vs }) => {
301 if elements.len() != vs.len() {
302 return Err(r.mismatch(format!(
303 "tuple arity: expected {}, found {}",
304 elements.len(),
305 vs.len()
306 )));
307 }
308 let mut out = Vec::with_capacity(vs.len());
309 for (i, (et, ev)) in elements.iter().zip(vs.iter()).enumerate() {
310 r.path.push(PathSegment::Index(i));
311 let rendered = drive(walk(r, graph, et, ev))?;
312 r.path.pop();
313 out.push(rendered);
314 }
315 Ok(Value::Array(out))
316 }
317
318 (SchemaType::List { element, .. }, SchemaValue::List { elements }) => {
319 let mut out = Vec::with_capacity(elements.len());
320 for (i, ev) in elements.iter().enumerate() {
321 r.path.push(PathSegment::Index(i));
322 let rendered = drive(walk(r, graph, element, ev))?;
323 r.path.pop();
324 out.push(rendered);
325 }
326 Ok(Value::Array(out))
327 }
328
329 (
330 SchemaType::FixedList {
331 element, length, ..
332 },
333 SchemaValue::FixedList { elements },
334 ) => {
335 if elements.len() as u32 != *length {
336 return Err(r.mismatch(format!(
337 "fixed list length: expected {}, found {}",
338 length,
339 elements.len()
340 )));
341 }
342 let mut out = Vec::with_capacity(elements.len());
343 for (i, ev) in elements.iter().enumerate() {
344 r.path.push(PathSegment::Index(i));
345 let rendered = drive(walk(r, graph, element, ev))?;
346 r.path.pop();
347 out.push(rendered);
348 }
349 Ok(Value::Array(out))
350 }
351
352 (SchemaType::Map { key, value, .. }, SchemaValue::Map { entries }) => {
353 let mut out = Vec::with_capacity(entries.len());
354 for (i, (k, v)) in entries.iter().enumerate() {
355 r.path.push(PathSegment::MapKey(i));
356 let rk = drive(walk(r, graph, key, k))?;
357 r.path.pop();
358 r.path.push(PathSegment::MapValue(i));
359 let rv = drive(walk(r, graph, value, v))?;
360 r.path.pop();
361 out.push(Value::Array(vec![rk, rv]));
362 }
363 Ok(Value::Array(out))
364 }
365
366 (SchemaType::Option { inner, .. }, SchemaValue::Option { inner: v }) => match v {
367 None => Ok(Value::Null),
368 Some(inner_value) => {
369 r.path.push(PathSegment::OptionInner);
370 let rendered = drive(walk(r, graph, inner, inner_value))?;
371 r.path.pop();
372 Ok(rendered)
373 }
374 },
375
376 (SchemaType::Result { spec, .. }, SchemaValue::Result(payload)) => {
377 encode_result(r, graph, spec, payload)
378 }
379
380 (SchemaType::Union { spec, .. }, SchemaValue::Union(payload)) => {
381 encode_union(r, graph, spec, payload)
382 }
383
384 (SchemaType::Future { .. }, _) | (SchemaType::Stream { .. }, _) => Err(
385 RenderError::Unsupported("future/stream values have no JSON representation"),
386 ),
387
388 (ty, value) => Err(r.mismatch(format!(
389 "shape mismatch: expected {}, found {}",
390 type_name(ty),
391 value_name(value)
392 ))),
393 }
394}
395
396fn encode_result(
397 r: &mut ToJsonRenderer,
398 graph: &SchemaGraph,
399 spec: &ResultSpec,
400 payload: &ResultValuePayload,
401) -> Result<Value, RenderError> {
402 let mut obj = Map::new();
403 match payload {
404 ResultValuePayload::Ok {
405 value: payload_value,
406 } => {
407 let v = match (spec.ok.as_deref(), payload_value.as_deref()) {
408 (None, None) => Value::Null,
409 (Some(ok_ty), Some(inner)) => {
410 r.path.push(PathSegment::Ok);
411 let v = drive(walk(r, graph, ok_ty, inner))?;
412 r.path.pop();
413 v
414 }
415 _ => {
416 return Err(r.mismatch("result ok payload presence mismatch".to_string()));
417 }
418 };
419 obj.insert("ok".to_string(), v);
420 }
421 ResultValuePayload::Err {
422 value: payload_value,
423 } => {
424 let v = match (spec.err.as_deref(), payload_value.as_deref()) {
425 (None, None) => Value::Null,
426 (Some(err_ty), Some(inner)) => {
427 r.path.push(PathSegment::Err);
428 let v = drive(walk(r, graph, err_ty, inner))?;
429 r.path.pop();
430 v
431 }
432 _ => {
433 return Err(r.mismatch("result err payload presence mismatch".to_string()));
434 }
435 };
436 obj.insert("err".to_string(), v);
437 }
438 }
439 Ok(Value::Object(obj))
440}
441
442fn encode_union(
443 r: &mut ToJsonRenderer,
444 graph: &SchemaGraph,
445 spec: &UnionSpec,
446 payload: &UnionValuePayload,
447) -> Result<Value, RenderError> {
448 let branch = find_branch(spec, &payload.tag)
449 .ok_or_else(|| r.mismatch(format!("unknown union branch tag `{}`", payload.tag)))?;
450 r.path.push(PathSegment::Union(payload.tag.clone()));
452 let rendered = drive(walk(r, graph, &branch.body, &payload.body))?;
453 r.path.pop();
454 if !rule_matches(&branch.discriminator, &rendered)? {
458 return Err(RenderError::UnionTagMismatch {
459 tag: payload.tag.clone(),
460 reason: format!(
461 "encoded body does not satisfy discriminator {}",
462 rule_label(&branch.discriminator)
463 ),
464 });
465 }
466 Ok(rendered)
467}
468
469fn find_branch<'a>(spec: &'a UnionSpec, tag: &str) -> Option<&'a UnionBranch> {
470 spec.branches.iter().find(|b| b.tag == tag)
471}
472
473impl ToJsonRenderer {
474 fn mismatch(&self, reason: String) -> RenderError {
475 RenderError::ValueMismatch {
476 path: self.path.render(),
477 reason,
478 }
479 }
480}
481
482fn from_json_inner(
485 graph: &SchemaGraph,
486 ty: &SchemaType,
487 json: &Value,
488 path: &mut PathStack,
489 visited: &mut HashSet<TypeId>,
490 policy: JsonDecodePolicy,
491) -> Result<SchemaValue, RenderError> {
492 let res = resolve_ref::<_, SchemaValue, RenderError>(graph, ty, visited, |graph, body| {
495 match from_json_body(graph, body, json, path, &mut HashSet::new(), policy) {
496 Ok(v) => Ok(v),
497 Err(e) => Err(WalkerError::Walker(e)),
498 }
499 });
500 drive_with_path(res, path)
501}
502
503fn drive_with_path(
504 res: Result<SchemaValue, WalkerError<RenderError>>,
505 path: &PathStack,
506) -> Result<SchemaValue, RenderError> {
507 match res {
508 Ok(v) => Ok(v),
509 Err(WalkerError::Walker(e)) => Err(e),
510 Err(WalkerError::RefCycle(id)) => Err(RenderError::ValueMismatch {
511 path: path.render(),
512 reason: format!("reference cycle through `{id}`"),
513 }),
514 Err(WalkerError::DanglingRef(id)) => Err(RenderError::ValueMismatch {
515 path: path.render(),
516 reason: format!("dangling reference `{id}`"),
517 }),
518 }
519}
520
521fn from_json_body(
522 graph: &SchemaGraph,
523 ty: &SchemaType,
524 json: &Value,
525 path: &mut PathStack,
526 _local_visited: &mut HashSet<TypeId>,
527 policy: JsonDecodePolicy,
528) -> Result<SchemaValue, RenderError> {
529 let mut visited: HashSet<TypeId> = HashSet::new();
534 if policy == JsonDecodePolicy::Untrusted
535 && let Some(kind) = HostManagedKind::from_type(ty)
536 {
537 return Err(mismatch(
538 path,
539 format!(
540 "host-managed capability `{}` cannot be constructed from external JSON",
541 kind.kind_name()
542 ),
543 ));
544 }
545
546 match ty {
547 SchemaType::Ref { .. } => unreachable!("ref resolved by resolve_ref"),
548 SchemaType::Bool { .. } => match json.as_bool() {
549 Some(b) => Ok(SchemaValue::Bool(b)),
550 None => Err(mismatch(path, "expected JSON boolean".to_string())),
551 },
552 SchemaType::S8 { .. } => {
553 check_int_range::<i8>(json, path)?;
554 Ok(SchemaValue::S8(json_i64(json, path)? as i8))
555 }
556 SchemaType::S16 { .. } => {
557 check_int_range::<i16>(json, path)?;
558 Ok(SchemaValue::S16(json_i64(json, path)? as i16))
559 }
560 SchemaType::S32 { .. } => {
561 check_int_range::<i32>(json, path)?;
562 Ok(SchemaValue::S32(json_i64(json, path)? as i32))
563 }
564 SchemaType::S64 { .. } => Ok(SchemaValue::S64(json_i64_string(json, path)?)),
565 SchemaType::U8 { .. } => {
566 check_int_range::<u8>(json, path)?;
567 Ok(SchemaValue::U8(json_u64(json, path)? as u8))
568 }
569 SchemaType::U16 { .. } => {
570 check_int_range::<u16>(json, path)?;
571 Ok(SchemaValue::U16(json_u64(json, path)? as u16))
572 }
573 SchemaType::U32 { .. } => {
574 check_int_range::<u32>(json, path)?;
575 Ok(SchemaValue::U32(json_u64(json, path)? as u32))
576 }
577 SchemaType::U64 { .. } => Ok(SchemaValue::U64(json_u64_string(json, path)?)),
578 SchemaType::F32 { .. } => {
579 let value = json_f64(json, path)?;
580 check_f32_in_range(value, path)?;
581 Ok(SchemaValue::F32(value as f32))
582 }
583 SchemaType::F64 { .. } => {
584 let value = json_f64(json, path)?;
588 if !value.is_finite() {
589 return Err(mismatch(path, "f64 must be finite".to_string()));
590 }
591 Ok(SchemaValue::F64(value))
592 }
593 SchemaType::Char { .. } => match json.as_str() {
594 Some(s) => {
595 let mut chars = s.chars();
596 match (chars.next(), chars.next()) {
597 (Some(c), None) => Ok(SchemaValue::Char(c)),
598 _ => Err(mismatch(
599 path,
600 "expected JSON string with exactly one char".to_string(),
601 )),
602 }
603 }
604 None => Err(mismatch(path, "expected JSON string".to_string())),
605 },
606 SchemaType::String { .. } => match json.as_str() {
607 Some(s) => Ok(SchemaValue::String(s.to_string())),
608 None => Err(mismatch(path, "expected JSON string".to_string())),
609 },
610
611 SchemaType::Text { .. } => {
612 let p = canonical::text::from_json(json)?;
613 Ok(SchemaValue::Text(p))
614 }
615 SchemaType::Binary { .. } => {
616 let p = canonical::binary::from_json(json)?;
617 Ok(SchemaValue::Binary(p))
618 }
619 SchemaType::Path { .. } => {
620 let s = canonical::path::from_json(json)?;
621 Ok(SchemaValue::Path { path: s })
622 }
623 SchemaType::Url { .. } => {
624 let s = canonical::url::from_json(json)?;
625 Ok(SchemaValue::Url { url: s })
626 }
627 SchemaType::Uuid { .. } => {
628 let value = canonical::uuid::from_json(json)?;
629 Ok(SchemaValue::Uuid(value))
630 }
631 SchemaType::Datetime { .. } => {
632 let dt = canonical::datetime::from_json(json)?;
633 Ok(SchemaValue::Datetime { value: dt })
634 }
635 SchemaType::Duration { .. } => {
636 let p = canonical::duration::from_json(json)?;
637 Ok(SchemaValue::Duration(p))
638 }
639 SchemaType::Quantity { .. } => {
640 let q = canonical::quantity::from_json(json)?;
641 Ok(SchemaValue::Quantity(q))
642 }
643 #[cfg(not(all(feature = "guest", not(feature = "host"))))]
644 SchemaType::Secret { .. } => {
645 let p = canonical::secret::from_json(json)?;
646 Ok(SchemaValue::Secret(p))
647 }
648 #[cfg(not(all(feature = "guest", not(feature = "host"))))]
649 SchemaType::QuotaToken { .. } => {
650 let p = canonical::quota_token::from_json(json)?;
651 Ok(SchemaValue::QuotaToken(p))
652 }
653 #[cfg(not(all(feature = "guest", not(feature = "host"))))]
654 SchemaType::PermissionCard { .. } => {
655 let p = canonical::permission_card::from_json(json)?;
656 Ok(SchemaValue::PermissionCard(p))
657 }
658 #[cfg(all(feature = "guest", not(feature = "host")))]
659 SchemaType::Secret { .. }
660 | SchemaType::QuotaToken { .. }
661 | SchemaType::PermissionCard { .. } => Err(RenderError::Unsupported(
662 "opaque host-managed capabilities cannot be constructed by a guest",
663 )),
664
665 SchemaType::Record { fields, .. } => {
666 let obj = json
667 .as_object()
668 .ok_or_else(|| mismatch(path, "expected JSON object for record".to_string()))?;
669 let known: HashSet<&str> = fields.iter().map(|f| f.name.as_str()).collect();
672 for key in obj.keys() {
673 if !known.contains(key.as_str()) {
674 return Err(RenderError::UnexpectedField {
675 record: "anonymous".to_string(),
676 field: key.clone(),
677 });
678 }
679 }
680 let mut out = Vec::with_capacity(fields.len());
681 for field in fields.iter() {
682 match obj.get(&field.name) {
683 Some(value) => {
684 path.push(PathSegment::Field(field.name.clone()));
685 let v =
686 from_json_inner(graph, &field.body, value, path, &mut visited, policy)?;
687 path.pop();
688 out.push(v);
689 }
690 None if resolves_to_option(graph, &field.body) => {
691 out.push(SchemaValue::Option { inner: None });
692 }
693 None => {
694 return Err(mismatch(
695 path,
696 format!("missing record field `{}`", field.name),
697 ));
698 }
699 }
700 }
701 Ok(SchemaValue::Record { fields: out })
702 }
703
704 SchemaType::Variant { cases, .. } => match json {
705 Value::String(name) => {
706 let (idx, case) = find_variant_case(cases, name)
707 .ok_or_else(|| mismatch(path, format!("unknown variant case `{name}`")))?;
708 if case.payload.is_some() {
709 return Err(mismatch(
710 path,
711 format!("variant case `{name}` requires a payload"),
712 ));
713 }
714 Ok(SchemaValue::Variant(VariantValuePayload {
715 case: idx as u32,
716 payload: None,
717 }))
718 }
719 Value::Object(obj) if obj.len() == 1 => {
720 let (name, payload_json) = obj.iter().next().unwrap();
721 let (idx, case) = find_variant_case(cases, name)
722 .ok_or_else(|| mismatch(path, format!("unknown variant case `{name}`")))?;
723 let payload_ty = case.payload.as_ref().ok_or_else(|| {
724 mismatch(
725 path,
726 format!("variant case `{name}` does not accept a payload"),
727 )
728 })?;
729 path.push(PathSegment::Variant(name.clone()));
730 let inner =
731 from_json_inner(graph, payload_ty, payload_json, path, &mut visited, policy)?;
732 path.pop();
733 Ok(SchemaValue::Variant(VariantValuePayload {
734 case: idx as u32,
735 payload: Some(Box::new(inner)),
736 }))
737 }
738 _ => Err(mismatch(
739 path,
740 "expected JSON string or single-entry object for variant".to_string(),
741 )),
742 },
743
744 SchemaType::Enum { cases, .. } => {
745 let name = json
746 .as_str()
747 .ok_or_else(|| mismatch(path, "expected JSON string for enum".to_string()))?;
748 let idx = cases
749 .iter()
750 .position(|c| c == name)
751 .ok_or_else(|| mismatch(path, format!("unknown enum case `{name}`")))?;
752 Ok(SchemaValue::Enum { case: idx as u32 })
753 }
754
755 SchemaType::Flags { flags, .. } => {
756 let arr = json
757 .as_array()
758 .ok_or_else(|| mismatch(path, "expected JSON array for flags".to_string()))?;
759 let mut selected: HashSet<&str> = HashSet::new();
760 for item in arr {
761 let name = item.as_str().ok_or_else(|| {
762 mismatch(path, "expected JSON string in flags array".to_string())
763 })?;
764 if !flags.iter().any(|f| f == name) {
765 return Err(mismatch(path, format!("unknown flag `{name}`")));
766 }
767 if !selected.insert(name) {
768 return Err(RenderError::DuplicateFlag {
769 flag: name.to_string(),
770 });
771 }
772 }
773 let bits: Vec<bool> = flags
774 .iter()
775 .map(|f| selected.contains(f.as_str()))
776 .collect();
777 Ok(SchemaValue::Flags { bits })
778 }
779
780 SchemaType::Tuple { elements, .. } => {
781 let arr = json
782 .as_array()
783 .ok_or_else(|| mismatch(path, "expected JSON array for tuple".to_string()))?;
784 if arr.len() != elements.len() {
785 return Err(mismatch(
786 path,
787 format!(
788 "tuple arity: expected {}, found {}",
789 elements.len(),
790 arr.len()
791 ),
792 ));
793 }
794 let mut out = Vec::with_capacity(arr.len());
795 for (i, (et, ev)) in elements.iter().zip(arr.iter()).enumerate() {
796 path.push(PathSegment::Index(i));
797 let v = from_json_inner(graph, et, ev, path, &mut visited, policy)?;
798 path.pop();
799 out.push(v);
800 }
801 Ok(SchemaValue::Tuple { elements: out })
802 }
803
804 SchemaType::List { element, .. } => {
805 let arr = json
806 .as_array()
807 .ok_or_else(|| mismatch(path, "expected JSON array for list".to_string()))?;
808 let mut out = Vec::with_capacity(arr.len());
809 for (i, ev) in arr.iter().enumerate() {
810 path.push(PathSegment::Index(i));
811 let v = from_json_inner(graph, element, ev, path, &mut visited, policy)?;
812 path.pop();
813 out.push(v);
814 }
815 Ok(SchemaValue::List { elements: out })
816 }
817
818 SchemaType::FixedList {
819 element, length, ..
820 } => {
821 let arr = json
822 .as_array()
823 .ok_or_else(|| mismatch(path, "expected JSON array for fixed list".to_string()))?;
824 if arr.len() as u32 != *length {
825 return Err(mismatch(
826 path,
827 format!(
828 "fixed list length: expected {}, found {}",
829 length,
830 arr.len()
831 ),
832 ));
833 }
834 let mut out = Vec::with_capacity(arr.len());
835 for (i, ev) in arr.iter().enumerate() {
836 path.push(PathSegment::Index(i));
837 let v = from_json_inner(graph, element, ev, path, &mut visited, policy)?;
838 path.pop();
839 out.push(v);
840 }
841 Ok(SchemaValue::FixedList { elements: out })
842 }
843
844 SchemaType::Map { key, value, .. } => {
845 let arr = json
846 .as_array()
847 .ok_or_else(|| mismatch(path, "expected JSON array for map".to_string()))?;
848 let mut out: Vec<(SchemaValue, SchemaValue)> = Vec::with_capacity(arr.len());
849 for (i, item) in arr.iter().enumerate() {
850 let pair = item.as_array().ok_or_else(|| {
851 mismatch(path, "expected `[k, v]` array for map entry".to_string())
852 })?;
853 if pair.len() != 2 {
854 return Err(mismatch(
855 path,
856 format!(
857 "map entry must have exactly 2 elements, found {}",
858 pair.len()
859 ),
860 ));
861 }
862 path.push(PathSegment::MapKey(i));
863 let k = from_json_inner(graph, key, &pair[0], path, &mut visited, policy)?;
864 path.pop();
865 path.push(PathSegment::MapValue(i));
866 let v = from_json_inner(graph, value, &pair[1], path, &mut visited, policy)?;
867 path.pop();
868 out.push((k, v));
869 }
870 Ok(SchemaValue::Map { entries: out })
871 }
872
873 SchemaType::Option { inner, .. } => match json {
874 Value::Null => Ok(SchemaValue::Option { inner: None }),
875 other => {
876 path.push(PathSegment::OptionInner);
877 let v = from_json_inner(graph, inner, other, path, &mut visited, policy)?;
878 path.pop();
879 Ok(SchemaValue::Option {
880 inner: Some(Box::new(v)),
881 })
882 }
883 },
884
885 SchemaType::Result { spec, .. } => {
886 decode_result(graph, spec, json, path, &mut visited, policy)
887 }
888
889 SchemaType::Union { spec, .. } => decode_union(graph, spec, json, path, policy),
890
891 SchemaType::Future { .. } | SchemaType::Stream { .. } => Err(RenderError::Unsupported(
892 "future/stream values have no JSON representation",
893 )),
894 }
895}
896
897fn resolves_to_option(graph: &SchemaGraph, ty: &SchemaType) -> bool {
898 let mut current = ty;
899 let mut visited = HashSet::new();
900 loop {
901 match current {
902 SchemaType::Option { .. } => return true,
903 SchemaType::Ref { id, .. } if visited.insert(id.clone()) => match graph.lookup(id) {
904 Some(definition) => current = &definition.body,
905 None => return false,
906 },
907 _ => return false,
908 }
909 }
910}
911
912fn decode_result(
913 graph: &SchemaGraph,
914 spec: &ResultSpec,
915 json: &Value,
916 path: &mut PathStack,
917 visited: &mut HashSet<TypeId>,
918 policy: JsonDecodePolicy,
919) -> Result<SchemaValue, RenderError> {
920 let obj = json
921 .as_object()
922 .ok_or_else(|| mismatch(path, "expected JSON object for result".to_string()))?;
923 if obj.len() != 1 {
924 return Err(mismatch(
925 path,
926 "result object must have exactly one key `ok` or `err`".to_string(),
927 ));
928 }
929 let (key, payload_json) = obj.iter().next().unwrap();
930 match key.as_str() {
931 "ok" => {
932 let value = match (spec.ok.as_deref(), payload_json) {
933 (None, Value::Null) => None,
934 (Some(ok_ty), other) => {
935 path.push(PathSegment::Ok);
936 let v = from_json_inner(graph, ok_ty, other, path, visited, policy)?;
937 path.pop();
938 Some(Box::new(v))
939 }
940 _ => {
941 return Err(mismatch(
942 path,
943 "result ok payload presence mismatch".to_string(),
944 ));
945 }
946 };
947 Ok(SchemaValue::Result(ResultValuePayload::Ok { value }))
948 }
949 "err" => {
950 let value = match (spec.err.as_deref(), payload_json) {
951 (None, Value::Null) => None,
952 (Some(err_ty), other) => {
953 path.push(PathSegment::Err);
954 let v = from_json_inner(graph, err_ty, other, path, visited, policy)?;
955 path.pop();
956 Some(Box::new(v))
957 }
958 _ => {
959 return Err(mismatch(
960 path,
961 "result err payload presence mismatch".to_string(),
962 ));
963 }
964 };
965 Ok(SchemaValue::Result(ResultValuePayload::Err { value }))
966 }
967 other => Err(mismatch(path, format!("unexpected result key `{other}`"))),
968 }
969}
970
971fn decode_union(
972 graph: &SchemaGraph,
973 spec: &UnionSpec,
974 json: &Value,
975 path: &mut PathStack,
976 policy: JsonDecodePolicy,
977) -> Result<SchemaValue, RenderError> {
978 let mut matched: Vec<&UnionBranch> = Vec::new();
982 for branch in spec.branches.iter() {
983 if rule_matches(&branch.discriminator, json)? {
984 matched.push(branch);
985 }
986 }
987 match matched.as_slice() {
988 [] => Err(RenderError::UnionNoMatch),
989 [branch] => {
990 path.push(PathSegment::Union(branch.tag.clone()));
992 let body =
993 from_json_inner(graph, &branch.body, json, path, &mut HashSet::new(), policy)?;
994 path.pop();
995 Ok(SchemaValue::Union(UnionValuePayload {
996 tag: branch.tag.clone(),
997 body: Box::new(body),
998 }))
999 }
1000 multiple => Err(RenderError::UnionAmbiguous {
1001 matched: multiple.iter().map(|b| b.tag.clone()).collect(),
1002 }),
1003 }
1004}
1005
1006fn rule_matches(rule: &DiscriminatorRule, json: &Value) -> Result<bool, RenderError> {
1010 Ok(match rule {
1011 DiscriminatorRule::Prefix { prefix } => json
1012 .as_str()
1013 .map(|s| s.starts_with(prefix.as_str()))
1014 .unwrap_or(false),
1015 DiscriminatorRule::Suffix { suffix } => json
1016 .as_str()
1017 .map(|s| s.ends_with(suffix.as_str()))
1018 .unwrap_or(false),
1019 DiscriminatorRule::Contains { substring } => json
1020 .as_str()
1021 .map(|s| s.contains(substring.as_str()))
1022 .unwrap_or(false),
1023 #[cfg(not(feature = "regex"))]
1024 DiscriminatorRule::Regex { .. } => return Err(RenderError::Unsupported("feature `regex`")),
1025 #[cfg(feature = "regex")]
1026 DiscriminatorRule::Regex { regex } => match (json.as_str(), regex::Regex::new(regex)) {
1027 (Some(s), Ok(re)) => re.is_match(s),
1028 _ => false,
1029 },
1030 DiscriminatorRule::FieldEquals(disc) => json
1031 .as_object()
1032 .and_then(|obj| obj.get(disc.field_name.as_str()))
1033 .map(|v| match &disc.literal {
1034 None => true,
1035 Some(lit) => v.as_str().map(|s| s == lit.as_str()).unwrap_or(false),
1036 })
1037 .unwrap_or(false),
1038 DiscriminatorRule::FieldAbsent { field_name } => json
1039 .as_object()
1040 .map(|obj| !obj.contains_key(field_name.as_str()))
1041 .unwrap_or(false),
1042 })
1043}
1044
1045fn rule_label(rule: &DiscriminatorRule) -> String {
1046 match rule {
1047 DiscriminatorRule::Prefix { prefix } => format!("prefix `{prefix}`"),
1048 DiscriminatorRule::Suffix { suffix } => format!("suffix `{suffix}`"),
1049 DiscriminatorRule::Contains { substring } => format!("contains `{substring}`"),
1050 DiscriminatorRule::Regex { regex } => format!("regex `{regex}`"),
1051 DiscriminatorRule::FieldEquals(disc) => match &disc.literal {
1052 None => format!("field `{}` present", disc.field_name),
1053 Some(lit) => format!("field `{}` == `{lit}`", disc.field_name),
1054 },
1055 DiscriminatorRule::FieldAbsent { field_name } => format!("field `{field_name}` absent"),
1056 }
1057}
1058
1059fn find_variant_case<'a>(
1062 cases: &'a [VariantCaseType],
1063 name: &str,
1064) -> Option<(usize, &'a VariantCaseType)> {
1065 cases.iter().enumerate().find(|(_, c)| c.name == name)
1066}
1067
1068fn json_i64(json: &Value, path: &mut PathStack) -> Result<i64, RenderError> {
1069 json.as_i64()
1070 .ok_or_else(|| mismatch(path, "expected JSON integer".to_string()))
1071}
1072
1073fn json_u64(json: &Value, path: &mut PathStack) -> Result<u64, RenderError> {
1074 json.as_u64()
1075 .ok_or_else(|| mismatch(path, "expected JSON non-negative integer".to_string()))
1076}
1077
1078fn json_i64_string(json: &Value, path: &mut PathStack) -> Result<i64, RenderError> {
1079 let value = json
1080 .as_str()
1081 .ok_or_else(|| mismatch(path, "expected canonical signed integer string".to_string()))?;
1082 if !is_canonical_signed_integer(value) {
1083 return Err(mismatch(
1084 path,
1085 "expected canonical signed integer string".to_string(),
1086 ));
1087 }
1088 value
1089 .parse()
1090 .map_err(|_| mismatch(path, "signed 64-bit integer out of range".to_string()))
1091}
1092
1093fn json_u64_string(json: &Value, path: &mut PathStack) -> Result<u64, RenderError> {
1094 let value = json.as_str().ok_or_else(|| {
1095 mismatch(
1096 path,
1097 "expected canonical unsigned integer string".to_string(),
1098 )
1099 })?;
1100 if value != "0" && !canonical_nonzero_digits(value) {
1101 return Err(mismatch(
1102 path,
1103 "expected canonical unsigned integer string".to_string(),
1104 ));
1105 }
1106 value
1107 .parse()
1108 .map_err(|_| mismatch(path, "unsigned 64-bit integer out of range".to_string()))
1109}
1110
1111fn is_canonical_signed_integer(value: &str) -> bool {
1112 value == "0"
1113 || value
1114 .strip_prefix('-')
1115 .is_some_and(canonical_nonzero_digits)
1116 || canonical_nonzero_digits(value)
1117}
1118
1119fn canonical_nonzero_digits(value: &str) -> bool {
1120 value
1121 .as_bytes()
1122 .first()
1123 .is_some_and(|digit| matches!(digit, b'1'..=b'9'))
1124 && value.bytes().all(|digit| digit.is_ascii_digit())
1125}
1126
1127fn json_f64(json: &Value, path: &mut PathStack) -> Result<f64, RenderError> {
1128 json.as_f64()
1129 .ok_or_else(|| mismatch(path, "expected JSON number".to_string()))
1130}
1131
1132fn json_number_i64(value: i64) -> Value {
1133 Value::Number(Number::from(value))
1134}
1135
1136fn json_number_u64(value: u64) -> Value {
1137 Value::Number(Number::from(value))
1138}
1139
1140fn json_number_f64(value: f64, path: &PathStack) -> Result<Value, RenderError> {
1141 Number::from_f64(value)
1142 .map(Value::Number)
1143 .ok_or_else(|| RenderError::Json(format!("non-finite float at {}", path.render())))
1144}
1145
1146fn check_f32_in_range(value: f64, path: &mut PathStack) -> Result<(), RenderError> {
1150 if !value.is_finite() {
1151 return Err(mismatch(path, "f32 must be finite".to_string()));
1152 }
1153 if value.abs() > f32::MAX as f64 {
1154 return Err(mismatch(path, "f32 out of range".to_string()));
1155 }
1156 Ok(())
1157}
1158
1159fn check_int_range<T: TryFrom<i128>>(
1160 json: &Value,
1161 path: &mut PathStack,
1162) -> Result<(), RenderError> {
1163 let n = json
1164 .as_i64()
1165 .map(i128::from)
1166 .or_else(|| json.as_u64().map(i128::from))
1167 .ok_or_else(|| mismatch(path, "expected JSON integer".to_string()))?;
1168 T::try_from(n)
1169 .map(|_| ())
1170 .map_err(|_| mismatch(path, "integer out of range".to_string()))
1171}
1172
1173fn mismatch(path: &PathStack, reason: String) -> RenderError {
1174 RenderError::ValueMismatch {
1175 path: path.render(),
1176 reason,
1177 }
1178}
1179
1180fn type_name(ty: &SchemaType) -> &'static str {
1181 match ty {
1182 SchemaType::Ref { .. } => "ref",
1183 SchemaType::Bool { .. } => "bool",
1184 SchemaType::S8 { .. } => "s8",
1185 SchemaType::S16 { .. } => "s16",
1186 SchemaType::S32 { .. } => "s32",
1187 SchemaType::S64 { .. } => "s64",
1188 SchemaType::U8 { .. } => "u8",
1189 SchemaType::U16 { .. } => "u16",
1190 SchemaType::U32 { .. } => "u32",
1191 SchemaType::U64 { .. } => "u64",
1192 SchemaType::F32 { .. } => "f32",
1193 SchemaType::F64 { .. } => "f64",
1194 SchemaType::Char { .. } => "char",
1195 SchemaType::String { .. } => "string",
1196 SchemaType::Record { .. } => "record",
1197 SchemaType::Variant { .. } => "variant",
1198 SchemaType::Enum { .. } => "enum",
1199 SchemaType::Flags { .. } => "flags",
1200 SchemaType::Tuple { .. } => "tuple",
1201 SchemaType::List { .. } => "list",
1202 SchemaType::FixedList { .. } => "fixed-list",
1203 SchemaType::Map { .. } => "map",
1204 SchemaType::Option { .. } => "option",
1205 SchemaType::Result { .. } => "result",
1206 SchemaType::Text { .. } => "text",
1207 SchemaType::Binary { .. } => "binary",
1208 SchemaType::Path { .. } => "path",
1209 SchemaType::Url { .. } => "url",
1210 SchemaType::Uuid { .. } => "uuid",
1211 SchemaType::Datetime { .. } => "datetime",
1212 SchemaType::Duration { .. } => "duration",
1213 SchemaType::Quantity { .. } => "quantity",
1214 SchemaType::Union { .. } => "union",
1215 SchemaType::Secret { .. } => "secret",
1216 SchemaType::QuotaToken { .. } => "quota-token",
1217 SchemaType::PermissionCard { .. } => "permission-card",
1218 SchemaType::Future { .. } => "future",
1219 SchemaType::Stream { .. } => "stream",
1220 }
1221}
1222
1223fn value_name(value: &SchemaValue) -> &'static str {
1224 match value {
1225 SchemaValue::Bool(_) => "bool",
1226 SchemaValue::S8(_) => "s8",
1227 SchemaValue::S16(_) => "s16",
1228 SchemaValue::S32(_) => "s32",
1229 SchemaValue::S64(_) => "s64",
1230 SchemaValue::U8(_) => "u8",
1231 SchemaValue::U16(_) => "u16",
1232 SchemaValue::U32(_) => "u32",
1233 SchemaValue::U64(_) => "u64",
1234 SchemaValue::F32(_) => "f32",
1235 SchemaValue::F64(_) => "f64",
1236 SchemaValue::Char(_) => "char",
1237 SchemaValue::String(_) => "string",
1238 SchemaValue::Record { .. } => "record",
1239 SchemaValue::Variant(_) => "variant",
1240 SchemaValue::Enum { .. } => "enum",
1241 SchemaValue::Flags { .. } => "flags",
1242 SchemaValue::Tuple { .. } => "tuple",
1243 SchemaValue::List { .. } => "list",
1244 SchemaValue::FixedList { .. } => "fixed-list",
1245 SchemaValue::Map { .. } => "map",
1246 SchemaValue::Option { .. } => "option",
1247 SchemaValue::Result(_) => "result",
1248 SchemaValue::Text(_) => "text",
1249 SchemaValue::Binary(_) => "binary",
1250 SchemaValue::Path { .. } => "path",
1251 SchemaValue::Url { .. } => "url",
1252 SchemaValue::Uuid(_) => "uuid",
1253 SchemaValue::Datetime { .. } => "datetime",
1254 SchemaValue::Duration(_) => "duration",
1255 SchemaValue::Quantity(_) => "quantity",
1256 SchemaValue::Union(_) => "union",
1257 SchemaValue::Secret(_) => "secret",
1258 SchemaValue::QuotaToken(_) => "quota-token",
1259 SchemaValue::Stream(_) => "stream",
1260 SchemaValue::PermissionCard(_) => "permission-card",
1261 }
1262}
1263
1264#[derive(Clone, Debug)]
1267enum PathSegment {
1268 Field(String),
1269 Variant(String),
1270 Index(usize),
1271 OptionInner,
1272 Ok,
1273 Err,
1274 Union(String),
1275 MapKey(usize),
1276 MapValue(usize),
1277}
1278
1279struct PathStack {
1280 segments: Vec<PathSegment>,
1281}
1282
1283impl PathStack {
1284 fn new() -> Self {
1285 Self {
1286 segments: Vec::new(),
1287 }
1288 }
1289
1290 fn push(&mut self, segment: PathSegment) {
1291 self.segments.push(segment);
1292 }
1293
1294 fn pop(&mut self) {
1295 self.segments.pop();
1296 }
1297
1298 fn render(&self) -> String {
1299 let mut s = String::new();
1300 for seg in &self.segments {
1301 match seg {
1302 PathSegment::Field(n) => {
1303 s.push('.');
1304 s.push_str(n);
1305 }
1306 PathSegment::Variant(n) => {
1307 s.push('.');
1308 s.push_str(n);
1309 }
1310 PathSegment::Index(i) => {
1311 s.push('[');
1312 s.push_str(&i.to_string());
1313 s.push(']');
1314 }
1315 PathSegment::OptionInner => s.push_str(".some"),
1316 PathSegment::Ok => s.push_str(".ok"),
1317 PathSegment::Err => s.push_str(".err"),
1318 PathSegment::Union(n) => {
1319 s.push('.');
1320 s.push_str(n);
1321 }
1322 PathSegment::MapKey(i) => {
1323 s.push_str(".key[");
1324 s.push_str(&i.to_string());
1325 s.push(']');
1326 }
1327 PathSegment::MapValue(i) => {
1328 s.push_str(".value[");
1329 s.push_str(&i.to_string());
1330 s.push(']');
1331 }
1332 }
1333 }
1334 if s.is_empty() { "$".to_string() } else { s }
1335 }
1336}