diff --git a/schemars/src/_private.rs b/schemars/src/_private.rs index 86898567..1519ca2d 100644 --- a/schemars/src/_private.rs +++ b/schemars/src/_private.rs @@ -18,6 +18,8 @@ pub fn json_schema_for_flatten( } SemiRecursiveTransform(|schema: &mut Schema| { + // Always allow aditional/unevaluated properties, because the outer struct determines + // whether it denies unknown fields. if let Some(obj) = schema.as_object_mut() { if obj.get("additionalProperties").and_then(Value::as_bool) == Some(false) { obj.remove("additionalProperties"); @@ -214,13 +216,6 @@ pub fn flatten(schema: &mut Schema, other: Schema) { } } } - "additionalProperties" | "unevaluatedProperties" => { - // Even if an outer type has `deny_unknown_fields`, unknown fields - // may be accepted by the flattened type - if occupied.get() == &Value::Bool(false) { - *occupied.into_mut() = value2; - } - } "oneOf" | "anyOf" => { // `OccupiedEntry` currently has no `.remove_entry()` method :( let key = occupied.key().clone(); @@ -245,16 +240,49 @@ pub fn flatten(schema: &mut Schema, other: Schema) { match other.try_to_object() { Err(false) => {} Err(true) => { - schema - .ensure_object() - .insert("unevaluatedProperties".to_owned(), true.into()); + if let Some(obj) = schema.as_object_mut() { + if !obj.contains_key("additionalProperties") + && !obj.contains_key("unevaluatedProperties") + { + let key = if contains_immediate_subschema(obj) { + "unevaluatedProperties" + } else { + "additionalProperties" + }; + obj.insert(key.to_owned(), true.into()); + } + } } - Ok(obj2) => { + Ok(mut obj2) => { let obj1 = schema.ensure_object(); + // For complex merges, replace `additionalProperties` with `unevaluatedProperties` + // which usually "works out better". + normalise_additional_unevaluated_properties(obj1, &obj2); + normalise_additional_unevaluated_properties(&mut obj2, obj1); + for (key, value2) in obj2 { flatten_property(obj1, key, value2); } } } } + +fn normalise_additional_unevaluated_properties( + schema_obj1: &mut Map, + schema_obj2: &Map, +) { + if schema_obj1.contains_key("additionalProperties") + && (schema_obj2.contains_key("unevaluatedProperties") + || contains_immediate_subschema(schema_obj2)) + { + let ap = schema_obj1.remove("additionalProperties"); + schema_obj1.insert("unevaluatedProperties".to_owned(), ap.into()); + } +} + +fn contains_immediate_subschema(schema_obj: &Map) -> bool { + ["if", "then", "else", "allOf", "anyOf", "oneOf", "$ref"] + .into_iter() + .any(|k| schema_obj.contains_key(k)) +} diff --git a/schemars/src/json_schema_impls/maps.rs b/schemars/src/json_schema_impls/maps.rs index ff179d09..86d43c8b 100644 --- a/schemars/src/json_schema_impls/maps.rs +++ b/schemars/src/json_schema_impls/maps.rs @@ -21,7 +21,7 @@ macro_rules! map_impl { fn json_schema(generator: &mut SchemaGenerator) -> Schema { json_schema!({ "type": "object", - "unevaluatedProperties": generator.subschema_for::(), + "additionalProperties": generator.subschema_for::(), }) } } diff --git a/schemars/tests/enum_deny_unknown_fields.rs b/schemars/tests/enum_deny_unknown_fields.rs index 2979981e..ef56d058 100644 --- a/schemars/tests/enum_deny_unknown_fields.rs +++ b/schemars/tests/enum_deny_unknown_fields.rs @@ -17,8 +17,8 @@ struct Struct { bar: bool, } -// Outer container should always have unevaluatedProperties: false -// `Struct` variant should have unevaluatedProperties: false +// Outer container should always have additionalProperties: false +// `Struct` variant should have additionalProperties: false #[allow(dead_code)] #[derive(JsonSchema)] #[schemars(rename_all = "camelCase", deny_unknown_fields)] @@ -42,7 +42,7 @@ fn enum_external_tag() -> TestResult { test_default_generated_schema::("enum-external-duf") } -// Only `Struct` variant should have unevaluatedProperties: false +// Only `Struct` variant should have additionalProperties: false #[allow(dead_code)] #[derive(JsonSchema)] #[schemars(tag = "typeProperty", deny_unknown_fields)] @@ -65,7 +65,7 @@ fn enum_internal_tag() -> TestResult { test_default_generated_schema::("enum-internal-duf") } -// Only `Struct` variant should have unevaluatedProperties: false +// Only `Struct` variant should have additionalProperties: false #[allow(dead_code)] #[derive(JsonSchema)] #[schemars(untagged, deny_unknown_fields)] @@ -88,7 +88,7 @@ fn enum_untagged() -> TestResult { test_default_generated_schema::("enum-untagged-duf") } -// Outer container and `Struct` variant should have unevaluatedProperties: false +// Outer container and `Struct` variant should have additionalProperties: false #[allow(dead_code)] #[derive(JsonSchema)] #[schemars(tag = "t", content = "c", deny_unknown_fields)] diff --git a/schemars/tests/expected/enum-adjacent-tagged-duf.json b/schemars/tests/expected/enum-adjacent-tagged-duf.json index 6f3950cc..dfb3bb8b 100644 --- a/schemars/tests/expected/enum-adjacent-tagged-duf.json +++ b/schemars/tests/expected/enum-adjacent-tagged-duf.json @@ -15,7 +15,7 @@ "required": [ "t" ], - "unevaluatedProperties": false + "additionalProperties": false }, { "type": "object", @@ -28,7 +28,7 @@ }, "c": { "type": "object", - "unevaluatedProperties": { + "additionalProperties": { "type": "string" } } @@ -37,7 +37,7 @@ "t", "c" ], - "unevaluatedProperties": false + "additionalProperties": false }, { "type": "object", @@ -56,7 +56,7 @@ "t", "c" ], - "unevaluatedProperties": false + "additionalProperties": false }, { "type": "object", @@ -75,7 +75,7 @@ "t", "c" ], - "unevaluatedProperties": false + "additionalProperties": false }, { "type": "object", @@ -97,7 +97,7 @@ "type": "boolean" } }, - "unevaluatedProperties": false, + "additionalProperties": false, "required": [ "foo", "bar" @@ -108,7 +108,7 @@ "t", "c" ], - "unevaluatedProperties": false + "additionalProperties": false }, { "type": "object", @@ -138,7 +138,7 @@ "t", "c" ], - "unevaluatedProperties": false + "additionalProperties": false }, { "type": "object", @@ -153,7 +153,7 @@ "required": [ "t" ], - "unevaluatedProperties": false + "additionalProperties": false }, { "type": "object", @@ -173,7 +173,7 @@ "t", "c" ], - "unevaluatedProperties": false + "additionalProperties": false } ], "$defs": { diff --git a/schemars/tests/expected/enum-adjacent-tagged.json b/schemars/tests/expected/enum-adjacent-tagged.json index be374ae9..c631ae54 100644 --- a/schemars/tests/expected/enum-adjacent-tagged.json +++ b/schemars/tests/expected/enum-adjacent-tagged.json @@ -27,7 +27,7 @@ }, "c": { "type": "object", - "unevaluatedProperties": { + "additionalProperties": { "type": "string" } } diff --git a/schemars/tests/expected/enum-external-duf.json b/schemars/tests/expected/enum-external-duf.json index 8d123023..76be5b3f 100644 --- a/schemars/tests/expected/enum-external-duf.json +++ b/schemars/tests/expected/enum-external-duf.json @@ -14,7 +14,7 @@ "properties": { "stringMap": { "type": "object", - "unevaluatedProperties": { + "additionalProperties": { "type": "string" } } @@ -62,7 +62,7 @@ "type": "boolean" } }, - "unevaluatedProperties": false, + "additionalProperties": false, "required": [ "foo", "bar" diff --git a/schemars/tests/expected/enum-external.json b/schemars/tests/expected/enum-external.json index 08ffad5b..3c660fb9 100644 --- a/schemars/tests/expected/enum-external.json +++ b/schemars/tests/expected/enum-external.json @@ -14,7 +14,7 @@ "properties": { "stringMap": { "type": "object", - "unevaluatedProperties": { + "additionalProperties": { "type": "string" } } diff --git a/schemars/tests/expected/enum-internal-duf.json b/schemars/tests/expected/enum-internal-duf.json index 4056f903..73e4743b 100644 --- a/schemars/tests/expected/enum-internal-duf.json +++ b/schemars/tests/expected/enum-internal-duf.json @@ -23,7 +23,7 @@ "const": "StringMap" } }, - "unevaluatedProperties": { + "additionalProperties": { "type": "string" }, "required": [ @@ -79,7 +79,7 @@ "const": "Struct" } }, - "unevaluatedProperties": false, + "additionalProperties": false, "required": [ "typeProperty", "foo", diff --git a/schemars/tests/expected/enum-internal.json b/schemars/tests/expected/enum-internal.json index 436202ad..2fd9770f 100644 --- a/schemars/tests/expected/enum-internal.json +++ b/schemars/tests/expected/enum-internal.json @@ -22,7 +22,7 @@ "const": "StringMap" } }, - "unevaluatedProperties": { + "additionalProperties": { "type": "string" }, "required": [ diff --git a/schemars/tests/expected/enum-untagged-duf.json b/schemars/tests/expected/enum-untagged-duf.json index 65a20fc1..58bdbe16 100644 --- a/schemars/tests/expected/enum-untagged-duf.json +++ b/schemars/tests/expected/enum-untagged-duf.json @@ -7,7 +7,7 @@ }, { "type": "object", - "unevaluatedProperties": { + "additionalProperties": { "type": "string" } }, @@ -28,7 +28,7 @@ "type": "boolean" } }, - "unevaluatedProperties": false, + "additionalProperties": false, "required": [ "foo", "bar" diff --git a/schemars/tests/expected/enum-untagged.json b/schemars/tests/expected/enum-untagged.json index 87e3f2e0..643cd20c 100644 --- a/schemars/tests/expected/enum-untagged.json +++ b/schemars/tests/expected/enum-untagged.json @@ -7,7 +7,7 @@ }, { "type": "object", - "unevaluatedProperties": { + "additionalProperties": { "type": "string" } }, diff --git a/schemars/tests/expected/flattened_value.json b/schemars/tests/expected/flattened_value.json index 1b53cdbc..fe79d426 100644 --- a/schemars/tests/expected/flattened_value.json +++ b/schemars/tests/expected/flattened_value.json @@ -10,5 +10,5 @@ "required": [ "flag" ], - "unevaluatedProperties": true + "additionalProperties": true } \ No newline at end of file diff --git a/schemars/tests/expected/indexmap.json b/schemars/tests/expected/indexmap.json index f0369228..8ba90a85 100644 --- a/schemars/tests/expected/indexmap.json +++ b/schemars/tests/expected/indexmap.json @@ -5,7 +5,7 @@ "properties": { "map": { "type": "object", - "unevaluatedProperties": { + "additionalProperties": { "type": "boolean" } }, diff --git a/schemars/tests/expected/remote_derive_generic.json b/schemars/tests/expected/remote_derive_generic.json index 135a7ff4..bef4d6ac 100644 --- a/schemars/tests/expected/remote_derive_generic.json +++ b/schemars/tests/expected/remote_derive_generic.json @@ -14,7 +14,7 @@ }, "fake_map": { "type": "object", - "unevaluatedProperties": { + "additionalProperties": { "type": "array", "uniqueItems": true, "items": { diff --git a/schemars/tests/expected/schema_settings-2019_09.json b/schemars/tests/expected/schema_settings-2019_09.json index 25ab2f86..bc99f155 100644 --- a/schemars/tests/expected/schema_settings-2019_09.json +++ b/schemars/tests/expected/schema_settings-2019_09.json @@ -13,7 +13,7 @@ }, "values": { "type": "object", - "unevaluatedProperties": true + "additionalProperties": true }, "value": true, "inner": { diff --git a/schemars/tests/expected/schema_settings-2020_12.json b/schemars/tests/expected/schema_settings-2020_12.json index 68818f00..79640586 100644 --- a/schemars/tests/expected/schema_settings-2020_12.json +++ b/schemars/tests/expected/schema_settings-2020_12.json @@ -13,7 +13,7 @@ }, "values": { "type": "object", - "unevaluatedProperties": true + "additionalProperties": true }, "value": true, "inner": { diff --git a/schemars/tests/expected/struct-normal-additional-properties.json b/schemars/tests/expected/struct-normal-additional-properties.json index 30f0062e..2c55fb7d 100644 --- a/schemars/tests/expected/struct-normal-additional-properties.json +++ b/schemars/tests/expected/struct-normal-additional-properties.json @@ -17,7 +17,7 @@ ] } }, - "unevaluatedProperties": false, + "additionalProperties": false, "required": [ "foo", "bar" diff --git a/schemars/tests/expected/test_flattened_struct_deny_unknown_fields.json b/schemars/tests/expected/test_flattened_struct_deny_unknown_fields.json index 419576e7..1ac94b54 100644 --- a/schemars/tests/expected/test_flattened_struct_deny_unknown_fields.json +++ b/schemars/tests/expected/test_flattened_struct_deny_unknown_fields.json @@ -42,7 +42,7 @@ "type": "boolean" } }, - "unevaluatedProperties": false, + "additionalProperties": false, "required": [ "middle_field", "inner_field" diff --git a/schemars/tests/expected/validate.json b/schemars/tests/expected/validate.json index 568f2bd2..57c7f521 100644 --- a/schemars/tests/expected/validate.json +++ b/schemars/tests/expected/validate.json @@ -68,7 +68,7 @@ }, "map_contains": { "type": "object", - "unevaluatedProperties": { + "additionalProperties": { "type": "null" }, "required": [ diff --git a/schemars/tests/expected/validate_schemars_attrs.json b/schemars/tests/expected/validate_schemars_attrs.json index 0122e43f..5e7d31d7 100644 --- a/schemars/tests/expected/validate_schemars_attrs.json +++ b/schemars/tests/expected/validate_schemars_attrs.json @@ -68,7 +68,7 @@ }, "map_contains": { "type": "object", - "unevaluatedProperties": { + "additionalProperties": { "type": "null" }, "required": [ diff --git a/schemars/tests/flatten.rs b/schemars/tests/flatten.rs index ffd8d1fa..a05a5c0b 100644 --- a/schemars/tests/flatten.rs +++ b/schemars/tests/flatten.rs @@ -76,24 +76,6 @@ struct FlattenMap { value: BTreeMap, } -#[allow(dead_code)] -#[derive(JsonSchema)] -#[schemars(rename = "FlattenValue", deny_unknown_fields)] -struct FlattenValueDenyUnknownFields { - flag: bool, - #[serde(flatten)] - value: Value, -} - -#[allow(dead_code)] -#[derive(JsonSchema)] -#[schemars(rename = "FlattenValue", deny_unknown_fields)] -struct FlattenMapDenyUnknownFields { - flag: bool, - #[serde(flatten)] - value: BTreeMap, -} - #[test] fn test_flattened_value() -> TestResult { test_default_generated_schema::("flattened_value") @@ -105,18 +87,6 @@ fn test_flattened_map() -> TestResult { test_default_generated_schema::("flattened_value") } -#[test] -fn test_flattened_value_deny_unknown_fields() -> TestResult { - // intentionally using the same file as test_flattened_value, as the schema should be identical - test_default_generated_schema::("flattened_value") -} - -#[test] -fn test_flattened_map_deny_unknown_fields() -> TestResult { - // intentionally using the same file as test_flattened_value, as the schema should be identical - test_default_generated_schema::("flattened_value") -} - #[derive(JsonSchema)] pub struct OuterAllowUnknownFields { pub outer_field: bool, diff --git a/schemars/tests/struct_additional_properties.rs b/schemars/tests/struct_additional_properties.rs index 93a4f290..7fd64f33 100644 --- a/schemars/tests/struct_additional_properties.rs +++ b/schemars/tests/struct_additional_properties.rs @@ -2,8 +2,6 @@ mod util; use schemars::JsonSchema; use util::*; -// TODO rename file and test - #[allow(dead_code)] #[derive(JsonSchema)] #[serde(deny_unknown_fields)] diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs index 6906b9f0..5b1eb977 100644 --- a/schemars_derive/src/schema_exprs.rs +++ b/schemars_derive/src/schema_exprs.rs @@ -303,9 +303,9 @@ fn expr_for_adjacent_tagged_enum<'a>( }) }; - let set_unevaluated_properties = if deny_unknown_fields { + let set_additional_properties = if deny_unknown_fields { quote! { - "unevaluatedProperties": false, + "additionalProperties": false, } } else { TokenStream::new() @@ -324,7 +324,7 @@ fn expr_for_adjacent_tagged_enum<'a>( ], // As we're creating a "wrapper" object, we can honor the // disposition of deny_unknown_fields. - #set_unevaluated_properties + #set_additional_properties }) }; @@ -498,9 +498,9 @@ fn expr_for_struct( }) .collect(); - let set_unevaluated_properties = if deny_unknown_fields { + let set_additional_properties = if deny_unknown_fields { quote! { - "unevaluatedProperties": false, + "additionalProperties": false, } } else { TokenStream::new() @@ -510,7 +510,7 @@ fn expr_for_struct( #set_container_default let mut schema = schemars::json_schema!({ "type": "object", - #set_unevaluated_properties + #set_additional_properties }); #(#properties)* schema