1use crate::{Error, ErrorKind, Result, config::RequestOptions, transport::Transport};
23use serde::Deserialize;
24use serde_json::{Map, Value};
25use std::{collections::BTreeMap, sync::OnceLock};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum Mode {
31 Request,
33 Response,
44 ResponseWithoutDefaults,
55}
56impl Mode {
57 fn is_response(self) -> bool {
59 matches!(self, Self::Response | Self::ResponseWithoutDefaults)
60 }
61 fn inserts_defaults(self) -> bool {
63 !matches!(self, Self::ResponseWithoutDefaults)
64 }
65}
66
67const MAX_DEPTH: usize = 64;
68
69#[derive(Clone, Debug)]
71#[non_exhaustive]
72pub struct ValidationLimits {
73 pub max_depth: usize,
74 pub max_nodes: usize,
75 pub max_input_bytes: usize,
76 pub max_work: usize,
77 pub max_copied_bytes: usize,
78}
79impl Default for ValidationLimits {
80 fn default() -> Self {
81 Self {
82 max_depth: 64,
83 max_nodes: 100_000,
84 max_input_bytes: 8 * 1024 * 1024,
85 max_work: 1_000_000,
86 max_copied_bytes: 32 * 1024 * 1024,
87 }
88 }
89}
90impl ValidationLimits {
91 pub fn for_bytes(bytes: usize) -> Result<Self> {
92 let scale = bytes.div_ceil(8 * 1024 * 1024).max(1);
93 let value = Self {
94 max_nodes: 100_000usize.saturating_mul(scale).min(1_000_000),
95 max_work: 1_000_000usize.saturating_mul(scale).min(16_000_000),
96 max_input_bytes: bytes,
97 max_copied_bytes: bytes.saturating_mul(4).min(512 * 1024 * 1024),
98 ..Self::default()
99 };
100 value.validate()?;
101 Ok(value)
102 }
103 pub(crate) fn validate(&self) -> Result<()> {
104 if !(1..=128).contains(&self.max_depth)
105 || !(1..=1_000_000).contains(&self.max_nodes)
106 || !(1..=512 * 1024 * 1024).contains(&self.max_input_bytes)
107 || !(1..=16_000_000).contains(&self.max_work)
108 || self.max_copied_bytes < self.max_input_bytes
109 || self.max_copied_bytes > 2 * 1024 * 1024 * 1024
110 {
111 Err(Error::new(ErrorKind::Configuration))
112 } else {
113 Ok(())
114 }
115 }
116}
117
118#[derive(Deserialize)]
119struct Graph {
120 roots: BTreeMap<String, String>,
121 nodes: BTreeMap<String, Value>,
122}
123#[derive(Deserialize)]
124struct Semantic {
125 binding: String,
126 #[serde(default)]
127 default: Value,
128}
129struct Registry {
130 graph: Graph,
131 semantics: BTreeMap<String, Semantic>,
132}
133fn registry() -> Result<&'static Registry> {
134 static REGISTRY: OnceLock<std::result::Result<Registry, ()>> = OnceLock::new();
135 REGISTRY
136 .get_or_init(|| {
137 let graph: Graph = serde_json::from_str(include_str!("validation/schema_graph.json"))
138 .map_err(|_| ())?;
139 let semantics =
140 serde_json::from_str(include_str!("validation/semantics.json")).map_err(|_| ())?;
141 Ok(Registry { graph, semantics })
142 })
143 .as_ref()
144 .map_err(|_| invalid())
145}
146fn invalid() -> Error {
147 Error::new(ErrorKind::Validation)
148}
149fn limit() -> Error {
150 Error::new(ErrorKind::Limit)
151}
152fn sid<'a>(registry: &'a Registry, name: &'a str) -> Result<&'a str> {
153 if registry.graph.nodes.contains_key(name) {
154 Ok(name)
155 } else {
156 registry
157 .graph
158 .roots
159 .get(name)
160 .map(String::as_str)
161 .ok_or_else(invalid)
162 }
163}
164fn child<'a>(node: &'a Value, key: &str) -> Result<&'a str> {
165 node.get(key)
166 .and_then(|value| value.get("$ref"))
167 .and_then(Value::as_str)
168 .ok_or_else(invalid)
169}
170
171pub fn parse(schema: &str, input: Value, mode: Mode) -> Result<Value> {
173 parse_with_limits(schema, input, mode, &ValidationLimits::default())
174}
175
176pub fn parse_optional(schema: &str, input: Option<Value>, mode: Mode) -> Result<Option<Value>> {
179 parse_optional_with_limits(schema, input, mode, &ValidationLimits::default())
180}
181pub fn parse_with_limits(
183 schema: &str,
184 input: Value,
185 mode: Mode,
186 limits: &ValidationLimits,
187) -> Result<Value> {
188 parse_optional_with_limits(schema, Some(input), mode, limits)?.ok_or_else(invalid)
189}
190
191pub fn parse_one_of_with_limits(
194 schemas: &[&str],
195 input: Value,
196 mode: Mode,
197 limits: &ValidationLimits,
198) -> Result<Value> {
199 if let Err(error) = limits.validate().and_then(|()| preflight(&input, limits)) {
200 discard(input);
201 return Err(error);
202 }
203 if schemas.is_empty() || schemas.len() > 64 {
204 return Err(invalid());
205 }
206 let registry = registry()?;
207 let mut context = Context {
208 registry,
209 limits,
210 mode,
211 work: 0,
212 copied: 0,
213 };
214 for schema in schemas {
215 let schema = sid(registry, schema)?;
216 match context.walk(schema, Some(&input), 0) {
217 Ok(Some(value)) => return Ok(value),
218 Err(error) if error.kind() == ErrorKind::Limit => return Err(error),
219 _ => {}
220 }
221 }
222 Err(invalid())
223}
224
225#[cfg(feature = "gateway")]
229pub fn parse_multipart_with_limits(
230 schema: &str,
231 metadata: Value,
232 binaries: &[(&str, &BinaryPart)],
233 limits: &ValidationLimits,
234 max_file_bytes: usize,
235) -> Result<Value> {
236 if let Err(error) = limits
237 .validate()
238 .and_then(|()| preflight(&metadata, limits))
239 {
240 discard(metadata);
241 return Err(error);
242 }
243 if max_file_bytes == 0 || max_file_bytes > 512 * 1024 * 1024 {
244 return Err(Error::new(ErrorKind::Configuration));
245 }
246 if binaries.len() > 6 {
247 return Err(limit());
248 }
249 let registry = registry()?;
250 let schema = sid(registry, schema)?;
251 if !matches!(
252 schema,
253 "s004590" | "s004620" | "s004771" | "s004833" | "s004894"
254 ) {
255 return Err(invalid());
256 }
257 let mut schema = schema;
258 let mut node = registry.graph.nodes.get(schema).ok_or_else(invalid)?;
259 for _ in 0..MAX_DEPTH {
260 if node["kind"] != "ZodLazy" {
261 break;
262 }
263 schema = child(node, "target")?;
264 node = registry.graph.nodes.get(schema).ok_or_else(invalid)?;
265 }
266 if node["kind"] != "ZodObject" {
267 return Err(invalid());
268 }
269 let properties = node
270 .get("properties")
271 .and_then(Value::as_object)
272 .ok_or_else(invalid)?;
273 let values = metadata.as_object().ok_or_else(invalid)?;
274 let mut total = 0usize;
275 for (index, (name, file)) in binaries.iter().enumerate() {
276 if values.contains_key(*name) || binaries[..index].iter().any(|(old, _)| old == name) {
277 return Err(invalid());
278 }
279 let property = properties.get(*name).ok_or_else(invalid)?;
280 boundary(child(property, "schema")?, "native_binary")?;
281 file.validate(max_file_bytes)?;
282 total = total.checked_add(file.bytes().len()).ok_or_else(limit)?;
283 if total > max_file_bytes {
284 return Err(limit());
285 }
286 }
287 let mut context = Context {
288 registry,
289 limits,
290 mode: Mode::Request,
291 work: 0,
292 copied: 0,
293 };
294 context.tick(0)?;
295 let mut result = Map::new();
296 for (key, property) in properties {
297 let child_schema = child(property, "schema")?;
298 if binaries.iter().any(|(name, _)| *name == key) {
299 context.tick(1)?;
300 continue;
301 }
302 if let Some(value) = context.walk(child_schema, values.get(key), 1)? {
303 result.insert(context.copy_text(key)?, value);
304 }
305 }
306 let catchall = child(node, "catchall")?;
307 let catchall_never = registry
308 .graph
309 .nodes
310 .get(catchall)
311 .is_some_and(|node| node["kind"] == "ZodNever");
312 for (key, value) in values {
313 if properties.contains_key(key) {
314 continue;
315 }
316 let value = if !catchall_never {
317 context
318 .walk(catchall, Some(value), 1)?
319 .ok_or_else(invalid)?
320 } else if node["unknownFields"] == "passthrough" {
321 context.clone_value(value, 1)?
322 } else if node["unknownFields"] == "strict" {
323 return Err(invalid());
324 } else {
325 continue;
326 };
327 if key != "__proto__" {
328 result.insert(context.copy_text(key)?, value);
329 }
330 }
331 Ok(Value::Object(result))
332}
333pub fn parse_array(schema: &str, input: Value, mode: Mode) -> Result<Value> {
335 parse_array_with_limits(schema, input, mode, &ValidationLimits::default())
336}
337pub fn parse_array_with_limits(
339 schema: &str,
340 input: Value,
341 mode: Mode,
342 limits: &ValidationLimits,
343) -> Result<Value> {
344 if let Err(error) = limits.validate() {
345 discard(input);
346 return Err(error);
347 }
348 if let Err(error) = preflight(&input, limits) {
349 discard(input);
350 return Err(error);
351 }
352 let registry = registry()?;
353 let schema = sid(registry, schema)?;
354 let values = input.as_array().ok_or_else(invalid)?;
355 let mut context = Context {
356 registry,
357 mode,
358 work: 0,
359 copied: 0,
360 limits,
361 };
362 context.tick(0)?;
363 let mut result = Vec::with_capacity(values.len());
364 for value in values {
365 result.push(context.walk(schema, Some(value), 1)?.ok_or_else(invalid)?);
366 }
367 Ok(Value::Array(result))
368}
369pub fn parse_optional_with_limits(
371 schema: &str,
372 input: Option<Value>,
373 mode: Mode,
374 limits: &ValidationLimits,
375) -> Result<Option<Value>> {
376 if let Err(error) = limits.validate() {
377 if let Some(input) = input {
378 discard(input);
379 }
380 return Err(error);
381 }
382 if let Some(value) = &input
383 && let Err(error) = preflight(value, limits)
384 {
385 if let Some(value) = input {
386 discard(value);
387 }
388 return Err(error);
389 }
390 let registry = registry()?;
391 let schema = sid(registry, schema)?;
392 let mut context = Context {
393 registry,
394 mode,
395 work: 0,
396 copied: 0,
397 limits,
398 };
399 let output = context.walk(schema, input.as_ref(), 0)?;
400 Ok(if input.is_none() && !mode.inserts_defaults() {
401 None
402 } else {
403 output
404 })
405}
406
407fn preflight(value: &Value, limits: &ValidationLimits) -> Result<()> {
408 let mut stack = vec![(value, 0usize)];
409 let mut nodes = 0usize;
410 let mut bytes = 0usize;
411 while let Some((value, depth)) = stack.pop() {
412 nodes = nodes.checked_add(1).ok_or_else(limit)?;
413 if nodes > limits.max_nodes || depth > limits.max_depth {
414 return Err(limit());
415 }
416 match value {
417 Value::String(text) => bytes = bytes.checked_add(text.len()).ok_or_else(limit)?,
418 Value::Array(values) => {
419 if values.len() > limits.max_nodes.saturating_sub(nodes) {
420 return Err(limit());
421 }
422 stack.extend(values.iter().map(|value| (value, depth + 1)));
423 }
424 Value::Object(values) => {
425 if values.len() > limits.max_nodes.saturating_sub(nodes) {
426 return Err(limit());
427 }
428 for (key, value) in values {
429 bytes = bytes.checked_add(key.len()).ok_or_else(limit)?;
430 stack.push((value, depth + 1));
431 }
432 }
433 _ => {}
434 }
435 if bytes > limits.max_input_bytes {
436 return Err(limit());
437 }
438 }
439 Ok(())
440}
441pub(crate) fn discard(value: Value) {
443 let mut values = vec![value];
444 while let Some(value) = values.pop() {
445 match value {
446 Value::Array(children) => values.extend(children),
447 Value::Object(children) => values.extend(children.into_values()),
448 _ => {}
449 }
450 }
451}
452
453struct Context<'a> {
454 registry: &'static Registry,
455 limits: &'a ValidationLimits,
456 mode: Mode,
457 work: usize,
458 copied: usize,
459}
460impl Context<'_> {
461 fn tick(&mut self, depth: usize) -> Result<()> {
462 self.work = self.work.checked_add(1).ok_or_else(limit)?;
463 if depth > self.limits.max_depth || self.work > self.limits.max_work {
464 Err(limit())
465 } else {
466 Ok(())
467 }
468 }
469 fn copy_text(&mut self, text: &str) -> Result<String> {
470 self.copied = self.copied.checked_add(text.len()).ok_or_else(limit)?;
471 if self.copied > self.limits.max_copied_bytes {
472 return Err(limit());
473 }
474 Ok(text.to_owned())
475 }
476 fn clone_value(&mut self, value: &Value, depth: usize) -> Result<Value> {
477 self.tick(depth)?;
478 Ok(match value {
479 Value::String(text) => Value::String(self.copy_text(text)?),
480 Value::Array(values) => Value::Array(
481 values
482 .iter()
483 .map(|value| self.clone_value(value, depth + 1))
484 .collect::<Result<_>>()?,
485 ),
486 Value::Object(values) => {
487 let mut result = Map::new();
488 for (key, value) in values {
489 result.insert(self.copy_text(key)?, self.clone_value(value, depth + 1)?);
490 }
491 Value::Object(result)
492 }
493 value => value.clone(),
494 })
495 }
496 fn check_finite_json(&mut self, value: &Value, depth: usize) -> Result<()> {
497 self.tick(depth)?;
498 match value {
499 Value::Object(values) => {
500 for (key, value) in values {
501 if matches!(key.as_str(), "__proto__" | "constructor" | "prototype") {
502 return Err(invalid());
503 }
504 self.check_finite_json(value, depth + 1)?;
505 }
506 }
507 Value::Array(values) => {
508 for value in values {
509 self.check_finite_json(value, depth + 1)?;
510 }
511 }
512 Value::Number(value) if !value.as_f64().is_some_and(f64::is_finite) => {
513 return Err(invalid());
514 }
515 _ => {}
516 }
517 Ok(())
518 }
519 fn walk(&mut self, schema: &str, input: Option<&Value>, depth: usize) -> Result<Option<Value>> {
520 let registry = self.registry;
521 let mut schema = schema;
522 let mut input = input;
523 let mut effects = Vec::new();
524 let mut terminal = None;
525 let mut reached_core = false;
526 for _ in 0..64 {
529 self.tick(if input.is_none() {
532 depth.saturating_sub(1)
533 } else {
534 depth
535 })?;
536 let node = registry.graph.nodes.get(schema).ok_or_else(invalid)?;
537 match node
538 .get("kind")
539 .and_then(Value::as_str)
540 .ok_or_else(invalid)?
541 {
542 "ZodResponseCompatibility" => {
543 schema = child(
548 node,
549 if self.mode.is_response() {
550 "response"
551 } else {
552 "request"
553 },
554 )?;
555 }
556 "ZodOptional" if input.is_none() => {
557 if !self.mode.is_response()
558 && node.get("responseOnly") == Some(&Value::Bool(true))
559 {
560 return Err(invalid());
561 }
562 terminal = Some(None);
563 reached_core = true;
564 break;
565 }
566 "ZodNullable" if input.is_some_and(Value::is_null) => {
567 terminal = Some(Some(Value::Null));
568 reached_core = true;
569 break;
570 }
571 "ZodOptional" | "ZodNullable" => schema = child(node, "inner")?,
572 "ZodDefault" => {
573 input = input.or(Some(
574 ®istry.semantics.get(schema).ok_or_else(invalid)?.default,
575 ));
576 schema = child(node, "inner")?;
577 }
578 "ZodLazy" => schema = child(node, "target")?,
579 "ZodEffects" => {
580 effects.push(registry.semantics.get(schema).ok_or_else(invalid)?);
581 schema = child(node, "inner")?;
582 }
583 _ => {
584 reached_core = true;
585 break;
586 }
587 }
588 }
589 if !reached_core {
590 return Err(limit());
591 }
592 let node = registry.graph.nodes.get(schema).ok_or_else(invalid)?;
593 let kind = node
594 .get("kind")
595 .and_then(Value::as_str)
596 .ok_or_else(invalid)?;
597 let mut result = if let Some(value) = terminal {
598 value
599 } else {
600 (|| -> Result<Option<Value>> {
601 match kind {
602 "ZodPipeline" => {
603 let first = self.walk(child(node, "input")?, input, depth)?;
604 self.walk(child(node, "output")?, first.as_ref(), depth)
605 }
606 "ZodAny" | "ZodUnknown" => input
607 .map(|value| self.clone_value(value, depth))
608 .transpose(),
609 "ZodNever" | "ZodDate" => Err(invalid()),
610 "ZodObject" => self.object(node, input, depth),
611 "ZodArray" => {
612 let values = input.and_then(Value::as_array).ok_or_else(invalid)?;
613 for (key, exact) in [
614 ("minLength", false),
615 ("maxLength", false),
616 ("exactLength", true),
617 ] {
618 if let Some(length) = node
619 .get(key)
620 .and_then(|value| value.get("value"))
621 .and_then(Value::as_u64)
622 {
623 let length = usize::try_from(length).map_err(|_| invalid())?;
624 if (exact && values.len() != length)
625 || (key == "minLength" && values.len() < length)
626 || (key == "maxLength" && values.len() > length)
627 {
628 return Err(invalid());
629 }
630 }
631 }
632 let mut result = Vec::with_capacity(values.len());
633 for value in values {
634 result.push(
635 self.walk(child(node, "element")?, Some(value), depth + 1)?
636 .ok_or_else(invalid)?,
637 );
638 }
639 Ok(Some(Value::Array(result)))
640 }
641 "ZodRecord" => {
642 let values = input.and_then(Value::as_object).ok_or_else(invalid)?;
643 let mut result = Map::new();
644 for (key, value) in values {
645 let key_value = Value::String(self.copy_text(key)?);
646 self.walk(child(node, "key")?, Some(&key_value), depth + 1)?;
647 let normalized = self
648 .walk(child(node, "value")?, Some(value), depth + 1)?
649 .ok_or_else(invalid)?;
650 if self.mode.is_response()
651 || node.get("preserveReservedKeys") == Some(&Value::Bool(true))
652 || key != "__proto__"
653 {
654 result.insert(self.copy_text(key)?, normalized);
655 }
656 }
657 Ok(Some(Value::Object(result)))
658 }
659 "ZodUnion" => {
660 let variants = node
661 .get("variants")
662 .and_then(Value::as_array)
663 .ok_or_else(invalid)?;
664 if self.mode.is_response()
665 && input.is_some_and(Value::is_string)
666 && variants.iter().all(|variant| {
667 variant
668 .get("$ref")
669 .and_then(Value::as_str)
670 .and_then(|sid| registry.graph.nodes.get(sid))
671 .is_some_and(|node| {
672 node["kind"] == "ZodLiteral" && node["value"].is_string()
673 })
674 })
675 {
676 return input
677 .map(|value| self.clone_value(value, depth))
678 .transpose();
679 }
680 for variant in variants {
681 let variant = variant
682 .get("$ref")
683 .and_then(Value::as_str)
684 .ok_or_else(invalid)?;
685 match self.walk(variant, input, depth) {
686 Ok(value) => return Ok(value),
687 Err(error) if error.kind() == ErrorKind::Limit => {
688 return Err(error);
689 }
690 Err(_) => {}
691 }
692 }
693 Err(invalid())
694 }
695 "ZodDiscriminatedUnion" => {
696 let values = input.and_then(Value::as_object).ok_or_else(invalid)?;
697 let discriminator = node
698 .get("discriminator")
699 .and_then(Value::as_str)
700 .ok_or_else(invalid)?;
701 let value = match values.get(discriminator) {
702 Some(value) => value,
703 None if node.get("preserveUnknownResponse")
704 == Some(&Value::Bool(true)) =>
705 {
706 let variants = node
709 .get("variants")
710 .and_then(Value::as_array)
711 .ok_or_else(invalid)?;
712 let mut selected = None;
713 for variant in variants {
714 let schema = variant
715 .get("$ref")
716 .and_then(Value::as_str)
717 .ok_or_else(invalid)?;
718 match self.walk(schema, input, depth) {
719 Ok(value) if selected.is_none() => selected = Some(value),
720 Ok(_) => return Err(invalid()),
721 Err(error) if error.kind() == ErrorKind::Limit => {
722 return Err(error);
723 }
724 Err(_) => {}
725 }
726 }
727 return selected.ok_or_else(invalid);
728 }
729 None => return Err(invalid()),
730 };
731 let mapping = node
732 .get("discriminatorMapping")
733 .and_then(Value::as_array)
734 .ok_or_else(invalid)?;
735 let selected = mapping
736 .iter()
737 .find(|mapping| mapping.get("value") == Some(value));
738 if selected.is_none()
739 && self.mode.is_response()
740 && value.is_string()
741 && node.get("preserveUnknownResponse") == Some(&Value::Bool(true))
742 {
743 return input
744 .map(|value| self.clone_value(value, depth))
745 .transpose();
746 }
747 let selected = selected.ok_or_else(invalid)?;
748 self.walk(child(selected, "schema")?, input, depth)
749 }
750 "ZodIntersection" => {
751 let left = self.walk(child(node, "left")?, input, depth)?;
752 let right = self.walk(child(node, "right")?, input, depth)?;
753 match (left, right) {
754 (None, None) => Ok(None),
755 (Some(left), Some(right)) => merge(left, right).map(Some),
756 _ => Err(invalid()),
757 }
758 }
759 "ZodEnum" | "ZodNativeEnum" => {
760 let value = input.ok_or_else(invalid)?;
761 let values = node
762 .get("values")
763 .and_then(Value::as_array)
764 .ok_or_else(invalid)?;
765 if values.contains(value)
766 || (self.mode.is_response()
767 && ((value.is_string() && values.iter().any(Value::is_string))
768 || (value.is_number() && values.iter().any(Value::is_number))))
769 {
770 Ok(Some(self.clone_value(value, depth)?))
771 } else {
772 Err(invalid())
773 }
774 }
775 "ZodLiteral" => {
776 let value = input.ok_or_else(invalid)?;
777 if equal(value, node.get("value").ok_or_else(invalid)?) {
778 Ok(Some(self.clone_value(value, depth)?))
779 } else {
780 Err(invalid())
781 }
782 }
783 "ZodString" => {
784 let value = input.and_then(Value::as_str).ok_or_else(invalid)?;
785 check_string(
786 value,
787 node.get("checks")
788 .and_then(Value::as_array)
789 .ok_or_else(invalid)?,
790 self.mode,
791 )?;
792 Ok(Some(self.clone_value(input.ok_or_else(invalid)?, depth)?))
793 }
794 "ZodNumber" => {
795 let value = input.and_then(Value::as_f64).ok_or_else(invalid)?;
796 check_number(
797 value,
798 node.get("checks")
799 .and_then(Value::as_array)
800 .ok_or_else(invalid)?,
801 )?;
802 Ok(Some(self.clone_value(input.ok_or_else(invalid)?, depth)?))
803 }
804 "ZodBoolean" => {
805 if input.is_some_and(Value::is_boolean) {
806 Ok(input.cloned())
807 } else {
808 Err(invalid())
809 }
810 }
811 "ZodNull" => {
812 if input.is_some_and(Value::is_null) {
813 Ok(Some(Value::Null))
814 } else {
815 Err(invalid())
816 }
817 }
818 _ => Err(invalid()),
819 }
820 })()?
821 };
822 for effect in effects.into_iter().rev() {
823 if matches!(
824 effect.binding.as_str(),
825 "finite_json" | "finite_json_object"
826 ) {
827 let value = result.as_ref().ok_or_else(invalid)?;
828 if effect.binding == "finite_json_object" && !value.is_object() {
829 return Err(invalid());
830 }
831 self.check_finite_json(value, depth)?;
832 } else {
833 result = Some(apply(&effect.binding, result)?);
834 }
835 }
836 Ok(result)
837 }
838 fn object(
839 &mut self,
840 node: &Value,
841 input: Option<&Value>,
842 depth: usize,
843 ) -> Result<Option<Value>> {
844 let values = input.and_then(Value::as_object).ok_or_else(invalid)?;
845 let properties = node
846 .get("properties")
847 .and_then(Value::as_object)
848 .ok_or_else(invalid)?;
849 let mut result = Map::new();
850 for (key, property) in properties {
851 let schema = child(property, "schema")?;
852 let present = values.get(key);
853 if let Some(value) = self.walk(schema, present, depth + 1)?
856 && (self.mode.is_response()
857 || node.get("preserveReservedKeys") == Some(&Value::Bool(true))
858 || key != "__proto__")
859 {
860 let key = self.copy_text(key)?;
864 if present.is_some() || self.mode.inserts_defaults() {
865 result.insert(key, value);
866 }
867 }
868 }
869 let catchall = child(node, "catchall")?;
870 let catchall_never = self
871 .registry
872 .graph
873 .nodes
874 .get(catchall)
875 .is_some_and(|node| node["kind"] == "ZodNever");
876 for (key, value) in values {
877 if properties.contains_key(key) {
878 continue;
879 }
880 let value = if !catchall_never {
881 self.walk(catchall, Some(value), depth + 1)?
882 .ok_or_else(invalid)?
883 } else if self.mode.is_response() || node["unknownFields"] == "passthrough" {
884 self.clone_value(value, depth + 1)?
885 } else if node["unknownFields"] == "strict" {
886 return Err(invalid());
887 } else {
888 continue;
889 };
890 if self.mode.is_response()
891 || node.get("preserveReservedKeys") == Some(&Value::Bool(true))
892 || key != "__proto__"
893 {
894 result.insert(self.copy_text(key)?, value);
895 }
896 }
897 Ok(Some(Value::Object(result)))
898 }
899}
900
901fn equal(left: &Value, right: &Value) -> bool {
902 match (left.as_f64(), right.as_f64()) {
903 (Some(left), Some(right)) => left == right,
904 _ => left == right,
905 }
906}
907fn merge(left: Value, right: Value) -> Result<Value> {
908 match (left, right) {
909 (Value::Object(mut left), Value::Object(right)) => {
910 for (key, right) in right {
911 if let Some(old) = left.remove(&key) {
912 left.insert(key, merge(old, right)?);
913 } else {
914 left.insert(key, right);
915 }
916 }
917 Ok(Value::Object(left))
918 }
919 (Value::Array(left), Value::Array(right)) if left.len() == right.len() => Ok(Value::Array(
920 left.into_iter()
921 .zip(right)
922 .map(|(left, right)| merge(left, right))
923 .collect::<Result<_>>()?,
924 )),
925 (left, right) if equal(&left, &right) => Ok(left),
926 _ => Err(invalid()),
927 }
928}
929
930fn js_space(value: char) -> bool {
931 matches!(value,
932 '\u{0009}'..='\u{000d}' | '\u{0020}' | '\u{00a0}' | '\u{1680}'
933 | '\u{2000}'..='\u{200a}' | '\u{2028}' | '\u{2029}' | '\u{202f}'
934 | '\u{205f}' | '\u{3000}' | '\u{feff}'
935 )
936}
937fn object(value: &Value) -> Result<&Map<String, Value>> {
938 value.as_object().ok_or_else(invalid)
939}
940fn not_null(map: &Map<String, Value>, key: &str) -> bool {
941 map.get(key).is_some_and(|value| !value.is_null())
942}
943fn apply(binding: &str, value: Option<Value>) -> Result<Value> {
944 let value = value.ok_or_else(invalid)?;
945 let valid = match binding {
946 "native_transport" | "native_cancellation" | "native_binary" => false,
947 "nonblank" => value
948 .as_str()
949 .is_some_and(|value| !value.trim_matches(js_space).is_empty()),
950 "chart_bounds" => {
951 let value = object(&value)?;
952 [("totalUnitsMin", "totalUnitsMax"), ("costMin", "costMax")]
953 .into_iter()
954 .all(|(lower, upper)| {
955 match (
956 value.get(lower).and_then(Value::as_f64),
957 value.get(upper).and_then(Value::as_f64),
958 ) {
959 (Some(lower), Some(upper)) => lower <= upper,
960 _ => true,
961 }
962 })
963 }
964 "time_order" => {
965 let value = object(&value)?;
966 match (
967 value.get("start_time").and_then(Value::as_str),
968 value.get("end_time").and_then(Value::as_str),
969 ) {
970 (Some(start), Some(end)) => match (timestamp(start), timestamp(end)) {
971 (Some(start), Some(end)) => start <= end,
972 _ => false,
973 },
974 _ => true,
975 }
976 }
977 "nonempty_object" => value.as_object().is_some_and(|value| !value.is_empty()),
978 "secret_create" => {
979 let value = object(&value)?;
980 let allowed = !(value.get("allow_all_workspaces") == Some(&Value::Bool(true))
981 && value.contains_key("allowed_workspaces"));
982 let key = match value.get("manager_type").and_then(Value::as_str) {
983 Some("aws_sm") => "aws_auth_type",
984 Some("azure_kv") => "azure_auth_mode",
985 _ => "vault_auth_type",
986 };
987 allowed
988 && value
989 .get("auth_config")
990 .and_then(Value::as_object)
991 .is_some_and(|config| config.contains_key(key))
992 }
993 "secret_update" => {
994 let value = object(&value)?;
995 !(value.is_empty()
996 || value.get("allow_all_workspaces") == Some(&Value::Bool(true))
997 && value.contains_key("allowed_workspaces"))
998 }
999 "realtime_type" => value
1000 .get("type")
1001 .and_then(Value::as_str)
1002 .is_some_and(|value| !value.is_empty()),
1003 "usage_reset_exclusive" => {
1004 let value = object(&value)?;
1005 !not_null(value, "periodic_reset") || !not_null(value, "periodic_reset_days")
1006 }
1007 "rotation_exclusive" => {
1008 let value = object(&value)?;
1009 !not_null(value, "rotation_period") || !not_null(value, "next_rotation_at")
1010 }
1011 "guardrail_checks" => {
1012 let value = object(&value)?;
1013 value.get("target").and_then(Value::as_str) == Some("mcp_tools")
1014 || (value.contains_key("checks") && value.contains_key("actions"))
1015 }
1016 "profile_selector" => {
1017 let value = object(&value)?;
1018 ["profile_id", "profile_name"].into_iter().any(|key| {
1019 value
1020 .get(key)
1021 .and_then(Value::as_str)
1022 .is_some_and(|value| !value.is_empty())
1023 })
1024 }
1025 "tool_event" => {
1026 let value = object(&value)?;
1027 value.contains_key("input") || value.contains_key("output")
1028 }
1029 "identity" => true,
1030 "unwrap_data" => {
1031 return match value {
1032 Value::Object(mut values) => values.remove("data").ok_or_else(invalid),
1033 _ => Err(invalid()),
1034 };
1035 }
1036 "message_object" => {
1037 return match value {
1038 Value::String(message) => {
1039 let mut values = Map::new();
1040 values.insert("message".to_owned(), Value::String(message));
1041 Ok(Value::Object(values))
1042 }
1043 _ => Err(invalid()),
1044 };
1045 }
1046 _ => false,
1047 };
1048 if valid { Ok(value) } else { Err(invalid()) }
1049}
1050
1051fn patterns() -> &'static BTreeMap<String, regex::Regex> {
1052 static PATTERNS: OnceLock<BTreeMap<String, regex::Regex>> = OnceLock::new();
1053 PATTERNS.get_or_init(|| {
1054 let whitespace = concat!(
1055 r"[\x{0009}-\x{000d}\x{0020}\x{00a0}\x{1680}\x{2000}-\x{200a}",
1056 r"\x{2028}\x{2029}\x{202f}\x{205f}\x{3000}\x{feff}]",
1057 );
1058 [
1059 r"^[A-Za-z0-9][A-Za-z0-9_-]*$",
1060 r"^[^,\s]+__[^,\s]+$",
1061 r"^[a-zA-Z0-9_-]+$",
1062 r"^\d+$",
1063 ]
1064 .into_iter()
1065 .filter_map(|source| {
1066 let native = source.replace(r"\d", "[0-9]").replace(r"\s", whitespace);
1067 regex::RegexBuilder::new(&native)
1068 .size_limit(64 * 1024)
1069 .build()
1070 .ok()
1071 .map(|regex| (source.to_owned(), regex))
1072 })
1073 .collect()
1074 })
1075}
1076fn email(value: &str) -> bool {
1077 let Some((local, domain)) = value.split_once('@') else {
1078 return false;
1079 };
1080 let local_character = |byte: u8| byte.is_ascii_alphanumeric() || b"_'+-.".contains(&byte);
1081 if local.is_empty()
1082 || local.starts_with('.')
1083 || local.contains("..")
1084 || !local.bytes().all(local_character)
1085 || !local
1086 .as_bytes()
1087 .last()
1088 .is_some_and(|byte| byte.is_ascii_alphanumeric() || b"_+-".contains(byte))
1089 {
1090 return false;
1091 }
1092 let Some((labels, top_level)) = domain.rsplit_once('.') else {
1093 return false;
1094 };
1095 top_level.len() >= 2
1096 && top_level.bytes().all(|byte| byte.is_ascii_alphabetic())
1097 && labels.split('.').all(|part| {
1098 !part.is_empty()
1099 && part.as_bytes()[0].is_ascii_alphanumeric()
1100 && part
1101 .bytes()
1102 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
1103 })
1104}
1105fn datetime_pattern() -> Option<&'static regex::Regex> {
1106 static REGEX: OnceLock<Option<regex::Regex>> = OnceLock::new();
1107 REGEX
1108 .get_or_init(|| {
1109 regex::Regex::new(concat!(
1110 r"^([0-9]{4})-([0-9]{2})-([0-9]{2})T([01][0-9]|2[0-3]):([0-5][0-9])",
1111 r"(?::([0-5][0-9])(?:\.([0-9]+))?)?(Z|[+-][0-9]{2}:?[0-9]{2})$",
1112 ))
1113 .ok()
1114 })
1115 .as_ref()
1116}
1117fn datetime(value: &str) -> bool {
1118 let Some(captures) = datetime_pattern().and_then(|pattern| pattern.captures(value)) else {
1119 return false;
1120 };
1121 let year = captures[1].parse::<i32>().ok();
1122 let month = captures[2]
1123 .parse::<u8>()
1124 .ok()
1125 .and_then(|value| time::Month::try_from(value).ok());
1126 let day = captures[3].parse::<u8>().ok();
1127 match (year, month, day) {
1128 (Some(year), Some(month), Some(day)) => {
1129 time::Date::from_calendar_date(year, month, day).is_ok()
1130 }
1131 _ => false,
1132 }
1133}
1134fn timestamp(value: &str) -> Option<i128> {
1135 let captures = datetime_pattern()?.captures(value)?;
1136 let date = time::Date::from_calendar_date(
1137 captures[1].parse().ok()?,
1138 time::Month::try_from(captures[2].parse::<u8>().ok()?).ok()?,
1139 captures[3].parse().ok()?,
1140 )
1141 .ok()?;
1142 let seconds = captures
1143 .get(6)
1144 .map(|value| value.as_str().parse::<u8>())
1145 .transpose()
1146 .ok()?
1147 .unwrap_or(0);
1148 let millis = captures
1149 .get(7)
1150 .map(|value| {
1151 let mut text = value.as_str().chars().take(3).collect::<String>();
1152 while text.len() < 3 {
1153 text.push('0');
1154 }
1155 text.parse::<u16>()
1156 })
1157 .transpose()
1158 .ok()?
1159 .unwrap_or(0);
1160 let zone = &captures[8];
1161 let offset = if zone == "Z" {
1162 time::UtcOffset::UTC
1163 } else {
1164 let sign = if zone.starts_with('-') { -1 } else { 1 };
1165 let digits = zone[1..].replace(':', "");
1166 time::UtcOffset::from_hms(
1167 digits[..2].parse::<i8>().ok()? * sign,
1168 digits[2..].parse::<i8>().ok()? * sign,
1169 0,
1170 )
1171 .ok()?
1172 };
1173 Some(
1174 date.with_hms_milli(
1175 captures[4].parse().ok()?,
1176 captures[5].parse().ok()?,
1177 seconds,
1178 millis,
1179 )
1180 .ok()?
1181 .assume_offset(offset)
1182 .unix_timestamp_nanos()
1183 / 1_000_000,
1184 )
1185}
1186fn check_string(value: &str, checks: &[Value], mode: Mode) -> Result<()> {
1187 let length = value.encode_utf16().count() as u64;
1188 for check in checks {
1189 let kind = check
1190 .get("kind")
1191 .and_then(Value::as_str)
1192 .ok_or_else(invalid)?;
1193 let length = match check.get("unit") {
1194 None => length,
1195 Some(Value::String(unit)) if unit == "code_points" => value.chars().count() as u64,
1196 Some(_) => return Err(invalid()),
1197 };
1198 let bound = if matches!(kind, "min" | "max") {
1200 Some(
1201 check
1202 .get("value")
1203 .and_then(Value::as_u64)
1204 .ok_or_else(invalid)?,
1205 )
1206 } else {
1207 None
1208 };
1209 let regex = if kind == "regex" {
1210 let source = check
1211 .get("regex")
1212 .and_then(|value| value.get("$regexp"))
1213 .and_then(Value::as_str)
1214 .ok_or_else(invalid)?;
1215 Some(patterns().get(source).ok_or_else(invalid)?)
1216 } else {
1217 None
1218 };
1219 if mode.is_response()
1222 && (matches!(
1223 kind,
1224 "uuid" | "rfc3339" | "datetime" | "date" | "duration" | "email" | "url" | "regex"
1225 ) || (matches!(kind, "min" | "max")
1226 && check.get("requestOnly") == Some(&Value::Bool(true))))
1227 {
1228 continue;
1229 }
1230 let valid = match kind {
1231 "min" => length >= bound.ok_or_else(invalid)?,
1232 "max" => length <= bound.ok_or_else(invalid)?,
1233 "uuid" => crate::data::is_uuid(value),
1234 "email" => email(value),
1235 "url" => url::Url::parse(value).is_ok(),
1236 "datetime" => datetime(value),
1237 "duration" => duration(value),
1238 "date" => date(value),
1239 "rfc3339" => rfc3339(value),
1240 "regex" => regex.ok_or_else(invalid)?.is_match(value),
1241 _ => false,
1242 };
1243 if !valid {
1244 return Err(invalid());
1245 }
1246 }
1247 Ok(())
1248}
1249fn duration(value: &str) -> bool {
1252 static PATTERN: OnceLock<regex::Regex> = OnceLock::new();
1253 let pattern = PATTERN.get_or_init(|| {
1254 regex::Regex::new(r"^P(?:(?:[0-9]+Y)?(?:[0-9]+M)?(?:[0-9]+D)?(?:T(?:[0-9]+H)?(?:[0-9]+M)?(?:[0-9]+S)?)?|[0-9]+W)$")
1255 .expect("constant duration grammar")
1256 });
1257 value != "P" && !value.ends_with('T') && pattern.is_match(value)
1258}
1259
1260fn date(value: &str) -> bool {
1261 if value.len() != 10
1262 || !value.bytes().enumerate().all(|(i, byte)| {
1263 if i == 4 || i == 7 {
1264 byte == b'-'
1265 } else {
1266 byte.is_ascii_digit()
1267 }
1268 })
1269 {
1270 return false;
1271 }
1272 let Ok(year) = value[..4].parse::<i32>() else {
1273 return false;
1274 };
1275 let Ok(month) = value[5..7].parse::<u8>() else {
1276 return false;
1277 };
1278 let Ok(day) = value[8..].parse::<u8>() else {
1279 return false;
1280 };
1281 let Ok(month) = time::Month::try_from(month) else {
1282 return false;
1283 };
1284 time::Date::from_calendar_date(year, month, day).is_ok()
1285}
1286
1287fn rfc3339(value: &str) -> bool {
1288 static PATTERN: OnceLock<regex::Regex> = OnceLock::new();
1289 let pattern = PATTERN.get_or_init(|| {
1290 regex::Regex::new(r"^([0-9]{4}-[0-9]{2}-[0-9]{2})[Tt]([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.[0-9]+)?([Zz]|([+-])([0-9]{2}):([0-9]{2}))$")
1291 .expect("constant RFC 3339 grammar")
1292 });
1293 let Some(captures) = pattern.captures(value) else {
1294 return false;
1295 };
1296 if !date(&captures[1]) {
1297 return false;
1298 }
1299 let number = |index| {
1300 captures
1301 .get(index)
1302 .and_then(|v| v.as_str().parse::<i16>().ok())
1303 };
1304 let (Some(hour), Some(minute), Some(second)) = (number(2), number(3), number(4)) else {
1305 return false;
1306 };
1307 if hour > 23 || minute > 59 || second > 60 {
1308 return false;
1309 }
1310 let offset = if captures.get(6).is_some() {
1311 let (Some(hours), Some(minutes)) = (number(7), number(8)) else {
1312 return false;
1313 };
1314 if hours > 23 || minutes > 59 {
1315 return false;
1316 }
1317 (hours * 60 + minutes) * if &captures[6] == "-" { -1 } else { 1 }
1318 } else {
1319 0
1320 };
1321 second < 60 || (hour * 60 + minute - offset).rem_euclid(1440) == 1439
1322}
1323
1324fn check_number(value: f64, checks: &[Value]) -> Result<()> {
1325 if !value.is_finite() {
1326 return Err(invalid());
1327 }
1328 for check in checks {
1329 let valid = match check
1330 .get("kind")
1331 .and_then(Value::as_str)
1332 .ok_or_else(invalid)?
1333 {
1334 "int" => value.fract() == 0.0,
1335 "finite" => value.is_finite(),
1336 "min" => {
1337 let boundary = check["value"].as_f64().ok_or_else(invalid)?;
1338 if check["inclusive"] == true {
1339 value >= boundary
1340 } else {
1341 value > boundary
1342 }
1343 }
1344 "max" => {
1345 let boundary = check["value"].as_f64().ok_or_else(invalid)?;
1346 if check["inclusive"] == true {
1347 value <= boundary
1348 } else {
1349 value < boundary
1350 }
1351 }
1352 "multipleOf" => multiple_of(value, check["value"].as_f64().ok_or_else(invalid)?),
1353 _ => false,
1354 };
1355 if !valid {
1356 return Err(invalid());
1357 }
1358 }
1359 Ok(())
1360}
1361fn multiple_of(value: f64, step: f64) -> bool {
1362 fn decimal_count(value: f64) -> usize {
1363 ryu_js::Buffer::new()
1364 .format_finite(value)
1365 .split_once('.')
1366 .map(|(_, fraction)| fraction.len())
1367 .unwrap_or(0)
1368 }
1369 let decimals = decimal_count(value).max(decimal_count(step));
1370 if decimals > 100 {
1371 return false;
1372 }
1373 let integer = |value: f64| {
1374 let text = ryu_js::Buffer::new()
1375 .format_to_fixed(value, decimals as u8)
1376 .replace('.', "");
1377 let prefix = text
1378 .chars()
1379 .enumerate()
1380 .take_while(|(index, value)| {
1381 value.is_ascii_digit() || (*index == 0 && matches!(value, '-' | '+'))
1382 })
1383 .map(|(_, value)| value)
1384 .collect::<String>();
1385 prefix.parse::<f64>().ok()
1386 };
1387 match (integer(value), integer(step)) {
1388 (Some(value), Some(step)) => step != 0.0 && value % step == 0.0,
1389 _ => false,
1390 }
1391}
1392
1393#[derive(Clone, Copy, PartialEq, Eq)]
1395pub struct NativeDate {
1396 epoch_millis: i64,
1397}
1398impl NativeDate {
1399 pub fn from_epoch_millis(value: i64) -> Result<Self> {
1400 if value.unsigned_abs() > 8_640_000_000_000_000 {
1401 Err(invalid())
1402 } else {
1403 Ok(Self {
1404 epoch_millis: value,
1405 })
1406 }
1407 }
1408 pub fn epoch_millis(&self) -> i64 {
1409 self.epoch_millis
1410 }
1411}
1412impl std::fmt::Debug for NativeDate {
1413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1414 f.write_str("NativeDate([REDACTED])")
1415 }
1416}
1417impl serde::Serialize for NativeDate {
1418 fn serialize<S: serde::Serializer>(&self, _: S) -> std::result::Result<S::Ok, S::Error> {
1419 Err(serde::ser::Error::custom(
1420 "native date requires a typed adapter",
1421 ))
1422 }
1423}
1424impl<'de> Deserialize<'de> for NativeDate {
1425 fn deserialize<D: serde::Deserializer<'de>>(_: D) -> std::result::Result<Self, D::Error> {
1426 Err(serde::de::Error::custom(
1427 "native date cannot be decoded from JSON",
1428 ))
1429 }
1430}
1431
1432#[cfg(feature = "models")]
1434pub use crate::models::BinaryFile as BinaryPart;
1435
1436fn boundary(schema: &str, expected: &str) -> Result<()> {
1437 let registry = registry()?;
1438 let mut schema = sid(registry, schema)?;
1439 for _ in 0..MAX_DEPTH {
1440 let node = registry.graph.nodes.get(schema).ok_or_else(invalid)?;
1441 if expected == "date" && node["kind"] == "ZodDate" {
1442 return Ok(());
1443 }
1444 if let Some(semantic) = registry.semantics.get(schema) {
1445 return if semantic.binding == expected {
1446 Ok(())
1447 } else {
1448 Err(invalid())
1449 };
1450 }
1451 schema = child(node, "inner")?;
1452 }
1453 Err(limit())
1454}
1455#[cfg(feature = "models")]
1456pub fn validate_binary(schema: &str, value: &BinaryPart) -> Result<()> {
1457 boundary(schema, "native_binary")?;
1458 value.validate(512 * 1024 * 1024)
1459}
1460pub fn validate_transport(schema: &str, _value: &dyn Transport) -> Result<()> {
1461 boundary(schema, "native_transport")
1462}
1463pub fn validate_cancellation(schema: &str, _value: &RequestOptions) -> Result<()> {
1464 boundary(schema, "native_cancellation")
1465}
1466pub fn validate_date(schema: &str, _value: &NativeDate) -> Result<()> {
1467 boundary(schema, "date")
1468}
1469
1470#[cfg(test)]
1471mod tests;