Repository: GREsau/schemars
Pull request #315 (2024-08-13 19:16:16)

Fix flattening of `serde_json::Value`
Related to #133

It should behave similarly to flattening a `Map<String,Value>` in that it allows any properties


Patch:
diff --git a/schemars/src/_private.rs b/schemars/src/_private.rs
index c507c5f..f641c02 100644
--- a/schemars/src/_private.rs
+++ b/schemars/src/_private.rs
@@ -176,58 +176,43 @@ pub fn apply_inner_validation(schema: &mut Schema, f: fn(&mut Schema) -> ()) {
 }
 
 pub fn flatten(schema: &mut Schema, other: Schema) {
-    if let Value::Object(obj2) = other.to_value() {
-        let obj1 = schema.ensure_object();
-
-        for (key, value2) in obj2 {
-            match obj1.entry(key) {
-                Entry::Vacant(vacant) => {
-                    vacant.insert(value2);
-                }
-                Entry::Occupied(mut occupied) => {
-                    match occupied.key().as_str() {
-                        // This special "type" handling can probably be removed once the enum variant `with`/`schema_with` behaviour is fixed
-                        "type" => match (occupied.get_mut(), value2) {
-                            (Value::Array(a1), Value::Array(mut a2)) => {
-                                a2.retain(|v2| !a1.contains(v2));
-                                a1.extend(a2);
-                            }
-                            (v1, Value::Array(mut a2)) => {
-                                if !a2.contains(v1) {
-                                    a2.push(std::mem::take(v1));
-                                    *occupied.get_mut() = Value::Array(a2);
-                                }
-                            }
-                            (Value::Array(a1), v2) => {
-                                if !a1.contains(&v2) {
-                                    a1.push(v2);
-                                }
-                            }
-                            (v1, v2) => {
-                                if v1 != &v2 {
-                                    *occupied.get_mut() =
-                                        Value::Array(vec![std::mem::take(v1), v2]);
+    match other.try_to_object() {
+        Err(false) => {}
+        Err(true) => {
+            schema
+                .ensure_object()
+                .entry("additionalProperties")
+                .or_insert(true.into());
+        }
+        Ok(obj2) => {
+            let obj1 = schema.ensure_object();
+
+            for (key, value2) in obj2 {
+                match obj1.entry(key) {
+                    Entry::Vacant(vacant) => {
+                        vacant.insert(value2);
+                    }
+                    Entry::Occupied(occupied) => {
+                        match occupied.key().as_str() {
+                            "required" => {
+                                if let Value::Array(a1) = occupied.into_mut() {
+                                    if let Value::Array(a2) = value2 {
+                                        a1.extend(a2);
+                                    }
                                 }
                             }
-                        },
-                        "required" => {
-                            if let Value::Array(a1) = occupied.into_mut() {
-                                if let Value::Array(a2) = value2 {
-                                    a1.extend(a2);
+                            "properties" | "patternProperties" => {
+                                if let Value::Object(o1) = occupied.into_mut() {
+                                    if let Value::Object(o2) = value2 {
+                                        o1.extend(o2);
+                                    }
                                 }
                             }
-                        }
-                        "properties" | "patternProperties" => {
-                            if let Value::Object(o1) = occupied.into_mut() {
-                                if let Value::Object(o2) = value2 {
-                                    o1.extend(o2);
-                                }
+                            _ => {
+                                // leave the original value as it is (don't modify `schema`)
                             }
-                        }
-                        _ => {
-                            // leave the original value as it is (don't modify `schema`)
-                        }
-                    };
+                        };
+                    }
                 }
             }
         }


Test patch:
diff --git a/schemars/tests/expected/flattened_value.json b/schemars/tests/expected/flattened_value.json
new file mode 100644
index 0000000..fe79d42
--- /dev/null
+++ b/schemars/tests/expected/flattened_value.json
@@ -0,0 +1,14 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "title": "FlattenValue",
+  "type": "object",
+  "properties": {
+    "flag": {
+      "type": "boolean"
+    }
+  },
+  "required": [
+    "flag"
+  ],
+  "additionalProperties": true
+}
\ No newline at end of file
diff --git a/schemars/tests/flatten.rs b/schemars/tests/flatten.rs
index dcbf186..321c419 100644
--- a/schemars/tests/flatten.rs
+++ b/schemars/tests/flatten.rs
@@ -1,5 +1,7 @@
 mod util;
 use schemars::JsonSchema;
+use serde_json::Value;
+use std::collections::BTreeMap;
 use util::*;
 
 #[allow(dead_code)]
@@ -53,5 +55,34 @@ fn test_flat_schema() -> TestResult {
 
 #[test]
 fn test_flattened_schema() -> TestResult {
+    // intentionally using the same file as test_flat_schema, as the schema should be identical
     test_default_generated_schema::<Deep1>("flatten")
 }
+
+#[allow(dead_code)]
+#[derive(JsonSchema)]
+struct FlattenValue {
+    flag: bool,
+    #[serde(flatten)]
+    value: Value,
+}
+
+#[allow(dead_code)]
+#[derive(JsonSchema)]
+#[schemars(rename = "FlattenValue")]
+struct FlattenMap {
+    flag: bool,
+    #[serde(flatten)]
+    value: BTreeMap<String, Value>,
+}
+
+#[test]
+fn test_flattened_value() -> TestResult {
+    test_default_generated_schema::<FlattenValue>("flattened_value")
+}
+
+#[test]
+fn test_flattened_map() -> TestResult {
+    // intentionally using the same file as test_flattened_value, as the schema should be identical
+    test_default_generated_schema::<FlattenMap>("flattened_value")
+}