diff --git a/CHANGELOG.md b/CHANGELOG.md index 457fe5af..28cabce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Ignoring `$schema` in resolved references. +- Support integer-valued numbers for `maxItems`, `maxLength`, `maxProperties`, `maxContains`, `minItems`, `minLength`, `minProperties`, `minContains`. ### Deprecated diff --git a/bindings/python/CHANGELOG.md b/bindings/python/CHANGELOG.md index 8ad6f339..b68fbbd6 100644 --- a/bindings/python/CHANGELOG.md +++ b/bindings/python/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Ignoring ``$schema`` in resolved references. +- Support integer-valued numbers for `maxItems`, `maxLength`, `maxProperties`, `maxContains`, `minItems`, `minLength`, `minProperties`, `minContains`. ### Deprecated diff --git a/jsonschema/src/keywords/contains.rs b/jsonschema/src/keywords/contains.rs index 6066ce58..03338896 100644 --- a/jsonschema/src/keywords/contains.rs +++ b/jsonschema/src/keywords/contains.rs @@ -456,6 +456,30 @@ mod tests { use crate::tests_util; use serde_json::json; + #[test] + fn max_contains_with_decimal() { + tests_util::is_valid( + &json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": {"const": 1}, + "maxContains": 1.0 + }), + &json!([1]), + ); + } + + #[test] + fn min_contains_with_decimal() { + tests_util::is_valid( + &json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": {"const": 1}, + "minContains": 2.0 + }), + &json!([1, 1]), + ); + } + #[test] fn schema_path() { tests_util::assert_schema_path(&json!({"contains": {"const": 2}}), &json!([]), "/contains") diff --git a/jsonschema/src/keywords/helpers.rs b/jsonschema/src/keywords/helpers.rs index 70c27be9..537cf594 100644 --- a/jsonschema/src/keywords/helpers.rs +++ b/jsonschema/src/keywords/helpers.rs @@ -78,12 +78,21 @@ pub(crate) fn map_get_u64<'a>( value, 0.into(), ))), - None => Some(Err(ValidationError::single_type_error( - JSONPointer::default(), - context.clone().into_pointer(), - value, - PrimitiveType::Integer, - ))), + None => { + if let Some(value) = value.as_f64() { + if value.trunc() == value { + // NOTE: Imprecise cast as big integers are not supported yet + #[allow(clippy::cast_possible_truncation)] + return Some(Ok(value as u64)); + } + } + Some(Err(ValidationError::single_type_error( + JSONPointer::default(), + context.clone().into_pointer(), + value, + PrimitiveType::Integer, + ))) + } } } diff --git a/jsonschema/src/keywords/max_items.rs b/jsonschema/src/keywords/max_items.rs index f3b83c5f..68760268 100644 --- a/jsonschema/src/keywords/max_items.rs +++ b/jsonschema/src/keywords/max_items.rs @@ -4,6 +4,7 @@ use crate::{ keywords::{helpers::fail_on_non_positive_integer, CompilationResult}, paths::{JSONPointer, JsonPointerNode}, validator::Validate, + Draft, }; use serde_json::{Map, Value}; @@ -14,12 +15,27 @@ pub(crate) struct MaxItemsValidator { impl MaxItemsValidator { #[inline] - pub(crate) fn compile(schema: &Value, schema_path: JSONPointer) -> CompilationResult { + pub(crate) fn compile( + schema: &Value, + schema_path: JSONPointer, + draft: Draft, + ) -> CompilationResult { if let Some(limit) = schema.as_u64() { - Ok(Box::new(MaxItemsValidator { limit, schema_path })) - } else { - Err(fail_on_non_positive_integer(schema, schema_path)) + return Ok(Box::new(MaxItemsValidator { limit, schema_path })); } + if !matches!(draft, Draft::Draft4) { + if let Some(limit) = schema.as_f64() { + if limit.trunc() == limit { + #[allow(clippy::cast_possible_truncation)] + return Ok(Box::new(MaxItemsValidator { + // NOTE: Imprecise cast as big integers are not supported yet + limit: limit as u64, + schema_path, + })); + } + } + } + Err(fail_on_non_positive_integer(schema, schema_path)) } } @@ -65,7 +81,11 @@ pub(crate) fn compile<'a>( context: &CompilationContext, ) -> Option> { let schema_path = context.as_pointer_with("maxItems"); - Some(MaxItemsValidator::compile(schema, schema_path)) + Some(MaxItemsValidator::compile( + schema, + schema_path, + context.config.draft(), + )) } #[cfg(test)] @@ -73,6 +93,11 @@ mod tests { use crate::tests_util; use serde_json::json; + #[test] + fn with_decimal() { + tests_util::is_valid(&json!({"maxItems": 2.0}), &json!([1])); + } + #[test] fn schema_path() { tests_util::assert_schema_path(&json!({"maxItems": 1}), &json!([1, 2]), "/maxItems") diff --git a/jsonschema/src/keywords/max_length.rs b/jsonschema/src/keywords/max_length.rs index bdd35406..236ebf78 100644 --- a/jsonschema/src/keywords/max_length.rs +++ b/jsonschema/src/keywords/max_length.rs @@ -4,6 +4,7 @@ use crate::{ keywords::{helpers::fail_on_non_positive_integer, CompilationResult}, paths::{JSONPointer, JsonPointerNode}, validator::Validate, + Draft, }; use serde_json::{Map, Value}; @@ -14,12 +15,27 @@ pub(crate) struct MaxLengthValidator { impl MaxLengthValidator { #[inline] - pub(crate) fn compile(schema: &Value, schema_path: JSONPointer) -> CompilationResult { + pub(crate) fn compile( + schema: &Value, + schema_path: JSONPointer, + draft: Draft, + ) -> CompilationResult { if let Some(limit) = schema.as_u64() { - Ok(Box::new(MaxLengthValidator { limit, schema_path })) - } else { - Err(fail_on_non_positive_integer(schema, schema_path)) + return Ok(Box::new(MaxLengthValidator { limit, schema_path })); } + if !matches!(draft, Draft::Draft4) { + if let Some(limit) = schema.as_f64() { + if limit.trunc() == limit { + #[allow(clippy::cast_possible_truncation)] + return Ok(Box::new(MaxLengthValidator { + // NOTE: Imprecise cast as big integers are not supported yet + limit: limit as u64, + schema_path, + })); + } + } + } + Err(fail_on_non_positive_integer(schema, schema_path)) } } @@ -65,7 +81,11 @@ pub(crate) fn compile<'a>( context: &CompilationContext, ) -> Option> { let schema_path = context.as_pointer_with("maxLength"); - Some(MaxLengthValidator::compile(schema, schema_path)) + Some(MaxLengthValidator::compile( + schema, + schema_path, + context.config.draft(), + )) } #[cfg(test)] @@ -73,6 +93,11 @@ mod tests { use crate::tests_util; use serde_json::json; + #[test] + fn with_decimal() { + tests_util::is_valid(&json!({"maxLength": 2.0}), &json!("f")); + } + #[test] fn schema_path() { tests_util::assert_schema_path(&json!({"maxLength": 1}), &json!("ab"), "/maxLength") diff --git a/jsonschema/src/keywords/max_properties.rs b/jsonschema/src/keywords/max_properties.rs index 3dd07d92..2c99b366 100644 --- a/jsonschema/src/keywords/max_properties.rs +++ b/jsonschema/src/keywords/max_properties.rs @@ -4,6 +4,7 @@ use crate::{ keywords::{helpers::fail_on_non_positive_integer, CompilationResult}, paths::{JSONPointer, JsonPointerNode}, validator::Validate, + Draft, }; use serde_json::{Map, Value}; @@ -14,12 +15,27 @@ pub(crate) struct MaxPropertiesValidator { impl MaxPropertiesValidator { #[inline] - pub(crate) fn compile(schema: &Value, schema_path: JSONPointer) -> CompilationResult { + pub(crate) fn compile( + schema: &Value, + schema_path: JSONPointer, + draft: Draft, + ) -> CompilationResult { if let Some(limit) = schema.as_u64() { - Ok(Box::new(MaxPropertiesValidator { limit, schema_path })) - } else { - Err(fail_on_non_positive_integer(schema, schema_path)) + return Ok(Box::new(MaxPropertiesValidator { limit, schema_path })); } + if !matches!(draft, Draft::Draft4) { + if let Some(limit) = schema.as_f64() { + if limit.trunc() == limit { + #[allow(clippy::cast_possible_truncation)] + return Ok(Box::new(MaxPropertiesValidator { + // NOTE: Imprecise cast as big integers are not supported yet + limit: limit as u64, + schema_path, + })); + } + } + } + Err(fail_on_non_positive_integer(schema, schema_path)) } } @@ -65,7 +81,11 @@ pub(crate) fn compile<'a>( context: &CompilationContext, ) -> Option> { let schema_path = context.as_pointer_with("maxProperties"); - Some(MaxPropertiesValidator::compile(schema, schema_path)) + Some(MaxPropertiesValidator::compile( + schema, + schema_path, + context.config.draft(), + )) } #[cfg(test)] @@ -73,6 +93,11 @@ mod tests { use crate::tests_util; use serde_json::json; + #[test] + fn with_decimal() { + tests_util::is_valid(&json!({"maxProperties": 2.0}), &json!({"foo": 1})); + } + #[test] fn schema_path() { tests_util::assert_schema_path( diff --git a/jsonschema/src/keywords/min_items.rs b/jsonschema/src/keywords/min_items.rs index bfa5f468..779ac7c5 100644 --- a/jsonschema/src/keywords/min_items.rs +++ b/jsonschema/src/keywords/min_items.rs @@ -4,6 +4,7 @@ use crate::{ keywords::{helpers::fail_on_non_positive_integer, CompilationResult}, paths::{JSONPointer, JsonPointerNode}, validator::Validate, + Draft, }; use serde_json::{Map, Value}; @@ -14,12 +15,27 @@ pub(crate) struct MinItemsValidator { impl MinItemsValidator { #[inline] - pub(crate) fn compile(schema: &Value, schema_path: JSONPointer) -> CompilationResult { + pub(crate) fn compile( + schema: &Value, + schema_path: JSONPointer, + draft: Draft, + ) -> CompilationResult { if let Some(limit) = schema.as_u64() { - Ok(Box::new(MinItemsValidator { limit, schema_path })) - } else { - Err(fail_on_non_positive_integer(schema, schema_path)) + return Ok(Box::new(MinItemsValidator { limit, schema_path })); } + if !matches!(draft, Draft::Draft4) { + if let Some(limit) = schema.as_f64() { + if limit.trunc() == limit { + #[allow(clippy::cast_possible_truncation)] + return Ok(Box::new(MinItemsValidator { + // NOTE: Imprecise cast as big integers are not supported yet + limit: limit as u64, + schema_path, + })); + } + } + } + Err(fail_on_non_positive_integer(schema, schema_path)) } } @@ -65,7 +81,11 @@ pub(crate) fn compile<'a>( context: &CompilationContext, ) -> Option> { let schema_path = context.as_pointer_with("minItems"); - Some(MinItemsValidator::compile(schema, schema_path)) + Some(MinItemsValidator::compile( + schema, + schema_path, + context.config.draft(), + )) } #[cfg(test)] @@ -73,6 +93,11 @@ mod tests { use crate::tests_util; use serde_json::json; + #[test] + fn with_decimal() { + tests_util::is_valid(&json!({"minItems": 1.0}), &json!([1, 2])); + } + #[test] fn schema_path() { tests_util::assert_schema_path(&json!({"minItems": 1}), &json!([]), "/minItems") diff --git a/jsonschema/src/keywords/min_length.rs b/jsonschema/src/keywords/min_length.rs index f9108aff..56e07083 100644 --- a/jsonschema/src/keywords/min_length.rs +++ b/jsonschema/src/keywords/min_length.rs @@ -4,6 +4,7 @@ use crate::{ keywords::{helpers::fail_on_non_positive_integer, CompilationResult}, paths::{JSONPointer, JsonPointerNode}, validator::Validate, + Draft, }; use serde_json::{Map, Value}; @@ -14,12 +15,27 @@ pub(crate) struct MinLengthValidator { impl MinLengthValidator { #[inline] - pub(crate) fn compile(schema: &Value, schema_path: JSONPointer) -> CompilationResult { + pub(crate) fn compile( + schema: &Value, + schema_path: JSONPointer, + draft: Draft, + ) -> CompilationResult { if let Some(limit) = schema.as_u64() { - Ok(Box::new(MinLengthValidator { limit, schema_path })) - } else { - Err(fail_on_non_positive_integer(schema, schema_path)) + return Ok(Box::new(MinLengthValidator { limit, schema_path })); } + if !matches!(draft, Draft::Draft4) { + if let Some(limit) = schema.as_f64() { + if limit.trunc() == limit { + #[allow(clippy::cast_possible_truncation)] + return Ok(Box::new(MinLengthValidator { + // NOTE: Imprecise cast as big integers are not supported yet + limit: limit as u64, + schema_path, + })); + } + } + } + Err(fail_on_non_positive_integer(schema, schema_path)) } } @@ -65,7 +81,11 @@ pub(crate) fn compile<'a>( context: &CompilationContext, ) -> Option> { let schema_path = context.as_pointer_with("minLength"); - Some(MinLengthValidator::compile(schema, schema_path)) + Some(MinLengthValidator::compile( + schema, + schema_path, + context.config.draft(), + )) } #[cfg(test)] @@ -73,6 +93,11 @@ mod tests { use crate::tests_util; use serde_json::json; + #[test] + fn with_decimal() { + tests_util::is_valid(&json!({"minLength": 2.0}), &json!("foo")); + } + #[test] fn schema_path() { tests_util::assert_schema_path(&json!({"minLength": 1}), &json!(""), "/minLength") diff --git a/jsonschema/src/keywords/min_properties.rs b/jsonschema/src/keywords/min_properties.rs index b3f44cce..99d7d039 100644 --- a/jsonschema/src/keywords/min_properties.rs +++ b/jsonschema/src/keywords/min_properties.rs @@ -4,6 +4,7 @@ use crate::{ keywords::{helpers::fail_on_non_positive_integer, CompilationResult}, paths::{JSONPointer, JsonPointerNode}, validator::Validate, + Draft, }; use serde_json::{Map, Value}; @@ -14,12 +15,27 @@ pub(crate) struct MinPropertiesValidator { impl MinPropertiesValidator { #[inline] - pub(crate) fn compile(schema: &Value, schema_path: JSONPointer) -> CompilationResult { + pub(crate) fn compile( + schema: &Value, + schema_path: JSONPointer, + draft: Draft, + ) -> CompilationResult { if let Some(limit) = schema.as_u64() { - Ok(Box::new(MinPropertiesValidator { limit, schema_path })) - } else { - Err(fail_on_non_positive_integer(schema, schema_path)) + return Ok(Box::new(MinPropertiesValidator { limit, schema_path })); } + if !matches!(draft, Draft::Draft4) { + if let Some(limit) = schema.as_f64() { + if limit.trunc() == limit { + #[allow(clippy::cast_possible_truncation)] + return Ok(Box::new(MinPropertiesValidator { + // NOTE: Imprecise cast as big integers are not supported yet + limit: limit as u64, + schema_path, + })); + } + } + } + Err(fail_on_non_positive_integer(schema, schema_path)) } } @@ -65,7 +81,11 @@ pub(crate) fn compile<'a>( context: &CompilationContext, ) -> Option> { let schema_path = context.as_pointer_with("minProperties"); - Some(MinPropertiesValidator::compile(schema, schema_path)) + Some(MinPropertiesValidator::compile( + schema, + schema_path, + context.config.draft(), + )) } #[cfg(test)] @@ -73,6 +93,11 @@ mod tests { use crate::tests_util; use serde_json::json; + #[test] + fn with_decimal() { + tests_util::is_valid(&json!({"minProperties": 1.0}), &json!({"foo": 1, "bar": 2})); + } + #[test] fn schema_path() { tests_util::assert_schema_path(