use indexmap::IndexMap;
use serde_json::{json, Map, Value};
use std::collections::HashMap;
use stillwater::Validation;
use crate::error::{SchemaError, SchemaErrors};
use crate::interop::ToJsonSchema;
use crate::path::JsonPath;
use super::traits::SchemaLike;
type CrossFieldValidator = Box<
dyn Fn(&ValidatedObject, &JsonPath) -> Validation<(), SchemaErrors> + Send + Sync + 'static,
>;
pub struct ValidatedObject {
fields: HashMap<String, Value>,
}
impl ValidatedObject {
pub fn get(&self, field: &str) -> Option<&Value> {
self.fields.get(field)
}
pub fn has(&self, field: &str) -> bool {
self.get(field).is_some_and(|v| !v.is_null())
}
}
struct FieldDef {
schema: Box<dyn super::traits::ValueValidator>,
required: bool,
default: Option<Value>,
}
enum AdditionalProperties {
Allow,
Deny,
Validate(Box<dyn super::traits::ValueValidator>),
}
pub struct ObjectSchema {
fields: IndexMap<String, FieldDef>,
additional_properties: AdditionalProperties,
type_error_message: Option<String>,
cross_field_validators: Vec<CrossFieldValidator>,
skip_on_field_errors: bool,
}
impl ObjectSchema {
pub fn new() -> Self {
Self {
fields: IndexMap::new(),
additional_properties: AdditionalProperties::Allow,
type_error_message: None,
cross_field_validators: Vec::new(),
skip_on_field_errors: true,
}
}
pub fn field<S>(mut self, name: impl Into<String>, schema: S) -> Self
where
S: SchemaLike + ToJsonSchema + 'static,
{
let name = name.into();
self.fields.insert(
name,
FieldDef {
schema: Box::new(SchemaWrapper(schema)),
required: true,
default: None,
},
);
self
}
pub fn optional<S>(mut self, name: impl Into<String>, schema: S) -> Self
where
S: SchemaLike + ToJsonSchema + 'static,
{
let name = name.into();
self.fields.insert(
name,
FieldDef {
schema: Box::new(SchemaWrapper(schema)),
required: false,
default: None,
},
);
self
}
pub fn default<S>(mut self, name: impl Into<String>, schema: S, default: Value) -> Self
where
S: SchemaLike + ToJsonSchema + 'static,
{
let name = name.into();
self.fields.insert(
name,
FieldDef {
schema: Box::new(SchemaWrapper(schema)),
required: false,
default: Some(default),
},
);
self
}
pub fn additional_properties<S>(mut self, setting: S) -> Self
where
S: Into<AdditionalPropertiesSetting>,
{
self.additional_properties = setting.into().0;
self
}
pub fn error(mut self, message: impl Into<String>) -> Self {
self.type_error_message = Some(message.into());
self
}
pub fn custom<F>(self, validator: F) -> Self
where
F: Fn(&ValidatedObject, &JsonPath) -> Validation<(), SchemaErrors> + Send + Sync + 'static,
{
let mut schema = self;
schema.cross_field_validators.push(Box::new(validator));
schema
}
pub fn skip_cross_field_on_errors(mut self, skip: bool) -> Self {
self.skip_on_field_errors = skip;
self
}
pub fn require_if<P>(
self,
condition_field: impl Into<String>,
predicate: P,
required_field: impl Into<String>,
) -> Self
where
P: Fn(&Value) -> bool + Send + Sync + 'static,
{
let condition_field = condition_field.into();
let required_field = required_field.into();
self.custom(move |obj, path| {
let condition_value = obj.get(&condition_field);
let required_value = obj.get(&required_field);
match (condition_value, required_value) {
(Some(cv), None) if predicate(cv) => Validation::Failure(SchemaErrors::single(
SchemaError::new(
path.push_field(&required_field),
format!(
"'{}' is required when '{}' matches condition",
required_field, condition_field
),
)
.with_code("conditional_required"),
)),
_ => Validation::Success(()),
}
})
}
pub fn mutually_exclusive(self, field1: impl Into<String>, field2: impl Into<String>) -> Self {
let field1 = field1.into();
let field2 = field2.into();
self.custom(move |obj, path| {
let has_field1 = obj.has(&field1);
let has_field2 = obj.has(&field2);
if has_field1 && has_field2 {
Validation::Failure(SchemaErrors::single(
SchemaError::new(
path.clone(),
format!("'{}' and '{}' are mutually exclusive", field1, field2),
)
.with_code("mutually_exclusive"),
))
} else {
Validation::Success(())
}
})
}
pub fn at_least_one_of<I, S>(self, fields: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let fields: Vec<String> = fields.into_iter().map(Into::into).collect();
self.custom(move |obj, path| {
let has_any = fields.iter().any(|f| obj.has(f));
if has_any {
Validation::Success(())
} else {
Validation::Failure(SchemaErrors::single(
SchemaError::new(
path.clone(),
format!("at least one of {:?} is required", fields),
)
.with_code("at_least_one_required"),
))
}
})
}
pub fn equal_fields(self, field1: impl Into<String>, field2: impl Into<String>) -> Self {
let field1 = field1.into();
let field2 = field2.into();
self.custom(move |obj, path| {
let value1 = obj.get(&field1);
let value2 = obj.get(&field2);
match (value1, value2) {
(Some(v1), Some(v2)) if v1 != v2 => Validation::Failure(SchemaErrors::single(
SchemaError::new(
path.push_field(&field2),
format!("'{}' must match '{}'", field2, field1),
)
.with_code("fields_not_equal"),
)),
_ => Validation::Success(()),
}
})
}
pub fn field_less_than(self, field1: impl Into<String>, field2: impl Into<String>) -> Self {
let field1 = field1.into();
let field2 = field2.into();
self.custom(move |obj, path| {
let value1 = obj.get(&field1);
let value2 = obj.get(&field2);
match (value1, value2) {
(Some(Value::Number(n1)), Some(Value::Number(n2))) => {
let Some(f1) = n1.as_f64() else {
return Validation::Success(());
};
let Some(f2) = n2.as_f64() else {
return Validation::Success(());
};
if f1 >= f2 {
Validation::Failure(SchemaErrors::single(
SchemaError::new(
path.push_field(&field1),
format!("'{}' must be less than '{}'", field1, field2),
)
.with_code("field_not_less_than"),
))
} else {
Validation::Success(())
}
}
(Some(Value::String(s1)), Some(Value::String(s2))) => {
if s1 >= s2 {
Validation::Failure(SchemaErrors::single(
SchemaError::new(
path.push_field(&field1),
format!("'{}' must be less than '{}'", field1, field2),
)
.with_code("field_not_less_than"),
))
} else {
Validation::Success(())
}
}
_ => Validation::Success(()),
}
})
}
pub fn field_less_or_equal(self, field1: impl Into<String>, field2: impl Into<String>) -> Self {
let field1 = field1.into();
let field2 = field2.into();
self.custom(move |obj, path| {
let value1 = obj.get(&field1);
let value2 = obj.get(&field2);
match (value1, value2) {
(Some(Value::Number(n1)), Some(Value::Number(n2))) => {
let Some(f1) = n1.as_f64() else {
return Validation::Success(());
};
let Some(f2) = n2.as_f64() else {
return Validation::Success(());
};
if f1 > f2 {
Validation::Failure(SchemaErrors::single(
SchemaError::new(
path.push_field(&field1),
format!("'{}' must be less than or equal to '{}'", field1, field2),
)
.with_code("field_not_less_or_equal"),
))
} else {
Validation::Success(())
}
}
(Some(Value::String(s1)), Some(Value::String(s2))) => {
if s1 > s2 {
Validation::Failure(SchemaErrors::single(
SchemaError::new(
path.push_field(&field1),
format!("'{}' must be less than or equal to '{}'", field1, field2),
)
.with_code("field_not_less_or_equal"),
))
} else {
Validation::Success(())
}
}
_ => Validation::Success(()),
}
})
}
pub fn validate(
&self,
value: &Value,
path: &JsonPath,
) -> Validation<Map<String, Value>, SchemaErrors> {
let obj = match value.as_object() {
Some(o) => o,
None => {
let message = self
.type_error_message
.clone()
.unwrap_or_else(|| "expected object".to_string());
return Validation::Failure(SchemaErrors::single(
SchemaError::new(path.clone(), message)
.with_code("invalid_type")
.with_got(value_type_name(value))
.with_expected("object"),
));
}
};
let mut errors = Vec::new();
let mut validated = Map::new();
for (name, field_def) in &self.fields {
let field_path = path.push_field(name);
match obj.get(name) {
Some(field_value) => {
match field_def.schema.validate_value(field_value, &field_path) {
Validation::Success(v) => {
validated.insert(name.clone(), v);
}
Validation::Failure(e) => {
errors.extend(e.into_iter());
}
}
}
None if field_def.required => {
errors.push(
SchemaError::new(
field_path,
format!("required field '{}' is missing", name),
)
.with_code("required")
.with_expected("value"),
);
}
None => {
if let Some(default) = &field_def.default {
validated.insert(name.clone(), default.clone());
}
}
}
}
for (key, value) in obj {
if !self.fields.contains_key(key) {
let field_path = path.push_field(key);
match &self.additional_properties {
AdditionalProperties::Allow => {
validated.insert(key.clone(), value.clone());
}
AdditionalProperties::Deny => {
errors.push(
SchemaError::new(field_path, format!("unknown field '{}'", key))
.with_code("additional_property"),
);
}
AdditionalProperties::Validate(schema) => {
match schema.validate_value(value, &field_path) {
Validation::Success(v) => {
validated.insert(key.clone(), v);
}
Validation::Failure(e) => {
errors.extend(e.into_iter());
}
}
}
}
}
}
if !self.skip_on_field_errors || errors.is_empty() {
let validated_obj = ValidatedObject {
fields: validated
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
};
for validator in &self.cross_field_validators {
if let Validation::Failure(e) = validator(&validated_obj, path) {
errors.extend(e.into_iter());
}
}
}
if errors.is_empty() {
Validation::Success(validated)
} else {
Validation::Failure(SchemaErrors::from_vec(errors))
}
}
}
impl Default for ObjectSchema {
fn default() -> Self {
Self::new()
}
}
impl SchemaLike for ObjectSchema {
type Output = Map<String, Value>;
fn validate(&self, value: &Value, path: &JsonPath) -> Validation<Self::Output, SchemaErrors> {
self.validate(value, path)
}
fn validate_to_value(&self, value: &Value, path: &JsonPath) -> Validation<Value, SchemaErrors> {
self.validate(value, path).map(Value::Object)
}
fn validate_with_context(
&self,
value: &Value,
path: &JsonPath,
context: &crate::validation::ValidationContext,
) -> Validation<Self::Output, SchemaErrors> {
let obj = match value.as_object() {
Some(o) => o,
None => {
let message = self
.type_error_message
.clone()
.unwrap_or_else(|| "expected object".to_string());
return Validation::Failure(SchemaErrors::single(
SchemaError::new(path.clone(), message)
.with_code("invalid_type")
.with_got(value_type_name(value))
.with_expected("object"),
));
}
};
let mut errors = Vec::new();
let mut validated = Map::new();
for (name, field_def) in &self.fields {
let field_path = path.push_field(name);
match obj.get(name) {
Some(field_value) => {
match field_def.schema.validate_value_with_context(
field_value,
&field_path,
context,
) {
Validation::Success(v) => {
validated.insert(name.clone(), v);
}
Validation::Failure(e) => {
errors.extend(e.into_iter());
}
}
}
None if field_def.required => {
errors.push(
SchemaError::new(
field_path,
format!("required field '{}' is missing", name),
)
.with_code("required")
.with_expected("value"),
);
}
None => {
if let Some(default) = &field_def.default {
validated.insert(name.clone(), default.clone());
}
}
}
}
for (key, value) in obj {
if !self.fields.contains_key(key) {
let field_path = path.push_field(key);
match &self.additional_properties {
AdditionalProperties::Allow => {
validated.insert(key.clone(), value.clone());
}
AdditionalProperties::Deny => {
errors.push(
SchemaError::new(field_path, format!("unknown field '{}'", key))
.with_code("additional_property"),
);
}
AdditionalProperties::Validate(schema) => {
match schema.validate_value_with_context(value, &field_path, context) {
Validation::Success(v) => {
validated.insert(key.clone(), v);
}
Validation::Failure(e) => {
errors.extend(e.into_iter());
}
}
}
}
}
}
if !self.skip_on_field_errors || errors.is_empty() {
let validated_obj = ValidatedObject {
fields: validated
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
};
for validator in &self.cross_field_validators {
if let Validation::Failure(e) = validator(&validated_obj, path) {
errors.extend(e.into_iter());
}
}
}
if errors.is_empty() {
Validation::Success(validated)
} else {
Validation::Failure(SchemaErrors::from_vec(errors))
}
}
fn validate_to_value_with_context(
&self,
value: &Value,
path: &JsonPath,
context: &crate::validation::ValidationContext,
) -> Validation<Value, SchemaErrors> {
self.validate_with_context(value, path, context)
.map(Value::Object)
}
fn collect_refs(&self, refs: &mut Vec<String>) {
for field_def in self.fields.values() {
field_def.schema.collect_refs(refs);
}
if let AdditionalProperties::Validate(schema) = &self.additional_properties {
schema.collect_refs(refs);
}
}
}
struct SchemaWrapper<S>(S);
impl<S: SchemaLike + ToJsonSchema> SchemaLike for SchemaWrapper<S> {
type Output = Value;
fn validate(&self, value: &Value, path: &JsonPath) -> Validation<Value, SchemaErrors> {
self.0.validate_to_value(value, path)
}
fn validate_to_value(&self, value: &Value, path: &JsonPath) -> Validation<Value, SchemaErrors> {
self.0.validate_to_value(value, path)
}
fn validate_with_context(
&self,
value: &Value,
path: &JsonPath,
context: &crate::validation::ValidationContext,
) -> Validation<Value, SchemaErrors> {
self.0.validate_to_value_with_context(value, path, context)
}
fn validate_to_value_with_context(
&self,
value: &Value,
path: &JsonPath,
context: &crate::validation::ValidationContext,
) -> Validation<Value, SchemaErrors> {
self.0.validate_to_value_with_context(value, path, context)
}
fn collect_refs(&self, refs: &mut Vec<String>) {
self.0.collect_refs(refs);
}
}
impl<S: SchemaLike + ToJsonSchema> ToJsonSchema for SchemaWrapper<S> {
fn to_json_schema(&self) -> Value {
self.0.to_json_schema()
}
}
pub struct AdditionalPropertiesSetting(AdditionalProperties);
impl From<bool> for AdditionalPropertiesSetting {
fn from(allow: bool) -> Self {
if allow {
AdditionalPropertiesSetting(AdditionalProperties::Allow)
} else {
AdditionalPropertiesSetting(AdditionalProperties::Deny)
}
}
}
impl<S: SchemaLike + ToJsonSchema + 'static> From<S> for AdditionalPropertiesSetting {
fn from(schema: S) -> Self {
AdditionalPropertiesSetting(AdditionalProperties::Validate(Box::new(SchemaWrapper(
schema,
))))
}
}
fn value_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
impl ToJsonSchema for ObjectSchema {
fn to_json_schema(&self) -> Value {
let mut properties = serde_json::Map::new();
let mut required = Vec::new();
for (name, field_def) in &self.fields {
properties.insert(name.clone(), field_def.schema.to_json_schema());
if field_def.required {
required.push(name.clone());
}
}
let mut schema = json!({
"type": "object",
"properties": properties,
});
if !required.is_empty() {
schema["required"] = json!(required);
}
match &self.additional_properties {
AdditionalProperties::Deny => {
schema["additionalProperties"] = json!(false);
}
AdditionalProperties::Allow => {}
AdditionalProperties::Validate(s) => {
schema["additionalProperties"] = s.to_json_schema();
}
}
schema
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{IntegerSchema, StringSchema};
use serde_json::json;
fn unwrap_success<T, E: std::fmt::Debug>(v: Validation<T, E>) -> T {
v.into_result().unwrap()
}
fn unwrap_failure<T: std::fmt::Debug, E>(v: Validation<T, E>) -> E {
v.into_result().unwrap_err()
}
#[test]
fn test_empty_object_schema() {
let schema = ObjectSchema::new();
let result = schema.validate(&json!({}), &JsonPath::root());
assert!(result.is_success());
}
#[test]
fn test_object_schema_rejects_non_object() {
let schema = ObjectSchema::new();
let result = schema.validate(&json!("not an object"), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().code, "invalid_type");
assert_eq!(errors.first().got, Some("string".to_string()));
let result = schema.validate(&json!(42), &JsonPath::root());
assert!(result.is_failure());
let result = schema.validate(&json!(null), &JsonPath::root());
assert!(result.is_failure());
let result = schema.validate(&json!([1, 2, 3]), &JsonPath::root());
assert!(result.is_failure());
}
#[test]
fn test_required_field() {
let schema = ObjectSchema::new().field("name", StringSchema::new());
let result = schema.validate(&json!({"name": "Alice"}), &JsonPath::root());
assert!(result.is_success());
let obj = unwrap_success(result);
assert_eq!(obj.get("name"), Some(&json!("Alice")));
let result = schema.validate(&json!({}), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().code, "required");
assert!(errors.first().message.contains("name"));
}
#[test]
fn test_required_field_invalid_value() {
let schema = ObjectSchema::new().field("age", IntegerSchema::new().positive());
let result = schema.validate(&json!({"age": -5}), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().code, "positive");
}
#[test]
fn test_optional_field() {
let schema = ObjectSchema::new().optional("nickname", StringSchema::new());
let result = schema.validate(&json!({}), &JsonPath::root());
assert!(result.is_success());
let obj = unwrap_success(result);
assert!(obj.get("nickname").is_none());
let result = schema.validate(&json!({"nickname": "Bob"}), &JsonPath::root());
assert!(result.is_success());
let obj = unwrap_success(result);
assert_eq!(obj.get("nickname"), Some(&json!("Bob")));
}
#[test]
fn test_optional_field_invalid_value() {
let schema = ObjectSchema::new().optional("age", IntegerSchema::new());
let result = schema.validate(&json!({"age": "not a number"}), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().code, "invalid_type");
}
#[test]
fn test_default_field() {
let schema = ObjectSchema::new().default("role", StringSchema::new(), json!("user"));
let result = schema.validate(&json!({}), &JsonPath::root());
assert!(result.is_success());
let obj = unwrap_success(result);
assert_eq!(obj.get("role"), Some(&json!("user")));
let result = schema.validate(&json!({"role": "admin"}), &JsonPath::root());
assert!(result.is_success());
let obj = unwrap_success(result);
assert_eq!(obj.get("role"), Some(&json!("admin")));
}
#[test]
fn test_additional_properties_allow() {
let schema = ObjectSchema::new()
.field("name", StringSchema::new())
.additional_properties(true);
let result = schema.validate(
&json!({"name": "Alice", "extra": "field"}),
&JsonPath::root(),
);
assert!(result.is_success());
let obj = unwrap_success(result);
assert_eq!(obj.get("extra"), Some(&json!("field")));
}
#[test]
fn test_additional_properties_deny() {
let schema = ObjectSchema::new()
.field("name", StringSchema::new())
.additional_properties(false);
let result = schema.validate(
&json!({"name": "Alice", "extra": "field"}),
&JsonPath::root(),
);
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().code, "additional_property");
assert!(errors.first().message.contains("extra"));
}
#[test]
fn test_additional_properties_validate() {
let schema = ObjectSchema::new()
.field("name", StringSchema::new())
.additional_properties(IntegerSchema::new());
let result = schema.validate(&json!({"name": "Alice", "count": 42}), &JsonPath::root());
assert!(result.is_success());
let result = schema.validate(
&json!({"name": "Alice", "count": "not a number"}),
&JsonPath::root(),
);
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().code, "invalid_type");
}
#[test]
fn test_multiple_fields() {
let schema = ObjectSchema::new()
.field("name", StringSchema::new().min_len(1))
.field("age", IntegerSchema::new().positive())
.optional("email", StringSchema::new());
let result = schema.validate(
&json!({"name": "Alice", "age": 30, "email": "alice@example.com"}),
&JsonPath::root(),
);
assert!(result.is_success());
}
#[test]
fn test_error_accumulation() {
let schema = ObjectSchema::new()
.field("name", StringSchema::new().min_len(5))
.field("age", IntegerSchema::new().positive());
let result = schema.validate(&json!({"name": "AB", "age": -5}), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.len(), 2);
assert!(errors.with_code("min_length").len() == 1);
assert!(errors.with_code("positive").len() == 1);
}
#[test]
fn test_error_accumulation_with_missing_fields() {
let schema = ObjectSchema::new()
.field("name", StringSchema::new())
.field("age", IntegerSchema::new());
let result = schema.validate(&json!({}), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.len(), 2);
assert_eq!(errors.with_code("required").len(), 2);
}
#[test]
fn test_path_tracking() {
let schema = ObjectSchema::new().field("user", StringSchema::new().min_len(5));
let result = schema.validate(&json!({"user": "AB"}), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().path.to_string(), "user");
}
#[test]
fn test_nested_object() {
let address_schema = ObjectSchema::new()
.field("street", StringSchema::new().min_len(1))
.field("city", StringSchema::new().min_len(1));
let user_schema = ObjectSchema::new()
.field("name", StringSchema::new())
.field("address", address_schema);
let result = user_schema.validate(
&json!({
"name": "Alice",
"address": {"street": "123 Main St", "city": "NYC"}
}),
&JsonPath::root(),
);
assert!(result.is_success());
let result = user_schema.validate(
&json!({
"name": "Alice",
"address": {"street": "", "city": ""}
}),
&JsonPath::root(),
);
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.len(), 2);
}
#[test]
fn test_deeply_nested_path_tracking() {
let inner = ObjectSchema::new().field("value", IntegerSchema::new().positive());
let middle = ObjectSchema::new().field("inner", inner);
let outer = ObjectSchema::new().field("middle", middle);
let result = outer.validate(
&json!({
"middle": {
"inner": {
"value": -5
}
}
}),
&JsonPath::root(),
);
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().path.to_string(), "middle.inner.value");
}
#[test]
fn test_custom_type_error_message() {
let schema = ObjectSchema::new().error("must be a user object");
let result = schema.validate(&json!("not an object"), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().message, "must be a user object");
}
#[test]
fn test_unicode_field_names() {
let schema = ObjectSchema::new()
.field("名前", StringSchema::new())
.field("年齢", IntegerSchema::new());
let result = schema.validate(&json!({"名前": "太郎", "年齢": 25}), &JsonPath::root());
assert!(result.is_success());
let result = schema.validate(&json!({}), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.len(), 2);
}
#[test]
fn test_empty_input_with_required_fields() {
let schema = ObjectSchema::new()
.field("a", StringSchema::new())
.field("b", IntegerSchema::new());
let result = schema.validate(&json!({}), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.len(), 2);
}
#[test]
fn test_field_order_preserved() {
let schema = ObjectSchema::new()
.field("z", StringSchema::new())
.field("a", StringSchema::new())
.field("m", StringSchema::new());
let result = schema.validate(&json!({}), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
let paths: Vec<_> = errors.iter().map(|e| e.path.to_string()).collect();
assert_eq!(paths, vec!["z", "a", "m"]);
}
#[test]
fn test_schema_like_trait_validate_to_value() {
let schema = ObjectSchema::new().field("name", StringSchema::new());
let result = schema.validate_to_value(&json!({"name": "Alice"}), &JsonPath::root());
assert!(result.is_success());
match result.into_result().unwrap() {
Value::Object(obj) => {
assert_eq!(obj.get("name"), Some(&json!("Alice")));
}
_ => panic!("Expected object"),
}
}
}