Repository: GREsau/schemars
Pull request #429 (2025-06-15 17:47:27)

Prefer `oneOf` when generating schema for serialized mixed-type sequences
Addresses #348


Patch:
diff --git a/schemars/src/ser.rs b/schemars/src/ser.rs
index 8460d97..81457c7 100644
--- a/schemars/src/ser.rs
+++ b/schemars/src/ser.rs
@@ -11,7 +11,7 @@ pub(crate) struct Serializer<'a> {
 
 pub(crate) struct SerializeSeq<'a> {
     generator: &'a mut SchemaGenerator,
-    items: Option<Schema>,
+    items: Vec<Schema>,
 }
 
 pub(crate) struct SerializeTuple<'a> {
@@ -182,7 +182,7 @@ impl<'a> serde::Serializer for Serializer<'a> {
     fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
         Ok(SerializeSeq {
             generator: self.generator,
-            items: None,
+            items: Vec::new(),
         })
     }
 
@@ -291,26 +291,33 @@ impl serde::ser::SerializeSeq for SerializeSeq<'_> {
     where
         T: serde::Serialize + ?Sized,
     {
-        if self.items != Some(true.into()) {
-            let schema = value.serialize(Serializer {
-                generator: self.generator,
-                include_title: false,
-            })?;
-            match &self.items {
-                None => self.items = Some(schema),
-                Some(items) => {
-                    if items != &schema {
-                        self.items = Some(true.into());
-                    }
-                }
-            }
+        if self.items.first() == Some(&true.into()) {
+            // Schema already allows any value, so no point in extending it
+            return Ok(());
+        }
+
+        let schema = value.serialize(Serializer {
+            generator: self.generator,
+            include_title: false,
+        })?;
+
+        if schema == true {
+            self.items = vec![schema];
+        } else if !self.items.contains(&schema) {
+            self.items.push(schema);
         }
 
         Ok(())
     }
 
-    fn end(self) -> Result<Self::Ok, Self::Error> {
-        let items = self.items.unwrap_or(true.into());
+    fn end(mut self) -> Result<Self::Ok, Self::Error> {
+        let items = match self.items.len() {
+            0 => true.into(),
+            1 => self.items.remove(0),
+            _ => json_schema!({
+                "anyOf": self.items
+            }),
+        };
 
         Ok(json_schema!({
             "type": "array",


Test patch:
diff --git a/schemars/tests/integration/from_value.rs b/schemars/tests/integration/from_value.rs
index 4b6a39e..d4ec5b0 100644
--- a/schemars/tests/integration/from_value.rs
+++ b/schemars/tests/integration/from_value.rs
@@ -2,7 +2,7 @@ use crate::prelude::*;
 use schemars::generate::SchemaSettings;
 use std::collections::BTreeMap;
 
-#[derive(JsonSchema, Deserialize, Serialize, Default, Clone)]
+#[derive(Deserialize, Serialize, Default, Clone)]
 #[serde(rename_all = "camelCase")]
 pub struct MyStruct {
     pub my_int: i32,
@@ -15,7 +15,7 @@ pub struct MyStruct {
     pub skip_if_none: Option<MyEnum>,
 }
 
-#[derive(JsonSchema, Deserialize, Serialize, Default, Clone)]
+#[derive(Deserialize, Serialize, Default, Clone)]
 pub struct MyInnerStruct {
     pub my_map: BTreeMap<String, f64>,
     pub my_vec: Vec<f32>,
diff --git a/schemars/tests/integration/snapshots/schemars/tests/integration/from_value.rs~json_value.json b/schemars/tests/integration/snapshots/schemars/tests/integration/from_value.rs~json_value.json
index 0818d4d..a324dc0 100644
--- a/schemars/tests/integration/snapshots/schemars/tests/integration/from_value.rs~json_value.json
+++ b/schemars/tests/integration/snapshots/schemars/tests/integration/from_value.rs~json_value.json
@@ -23,7 +23,16 @@
         },
         "mixed": {
           "type": "array",
-          "items": true
+          "items": {
+            "anyOf": [
+              {
+                "type": "integer"
+              },
+              {
+                "type": "boolean"
+              }
+            ]
+          }
         }
       }
     }
diff --git a/schemars/tests/integration/test_helper.rs b/schemars/tests/integration/test_helper.rs
index 8036488..307a0d1 100644
--- a/schemars/tests/integration/test_helper.rs
+++ b/schemars/tests/integration/test_helper.rs
@@ -12,7 +12,7 @@ use std::{
     sync::OnceLock,
 };
 
-pub struct TestHelper<T: JsonSchema> {
+pub struct TestHelper<T> {
     settings: SchemaSettings,
     name: String,
     phantom: PhantomData<T>,
@@ -23,9 +23,12 @@ pub struct TestHelper<T: JsonSchema> {
     validator: fn(&T) -> bool,
 }
 
-impl<T: JsonSchema> TestHelper<T> {
+impl<T> TestHelper<T> {
     /// Should be used via the `test!(SomeType)` macro
-    pub fn new(name: String, settings: SchemaSettings) -> Self {
+    pub fn new(name: String, settings: SchemaSettings) -> Self
+    where
+        T: JsonSchema,
+    {
         let de_schema = schema_for::<T>(&settings, Contract::Deserialize);
         let ser_schema = schema_for::<T>(&settings, Contract::Serialize);
         Self {
@@ -95,7 +98,10 @@ impl<T: JsonSchema> TestHelper<T> {
     }
 
     /// Checks that the schema generated for this type is identical to that of another type.
-    pub fn assert_identical<T2: JsonSchema>(&self) -> &Self {
+    pub fn assert_identical<T2: JsonSchema>(&self) -> &Self
+    where
+        T: JsonSchema,
+    {
         snapbox::assert_data_eq!(
             (&self.de_schema).into_json(),
             schema_for::<T2>(&self.settings, Contract::Deserialize)
@@ -159,7 +165,7 @@ fn build_validator(schema: &Schema) -> Validator {
         .expect("valid schema")
 }
 
-impl<T: JsonSchema + Serialize> TestHelper<T> {
+impl<T: Serialize> TestHelper<T> {
     /// Checks that the "serialize" schema allows the given sample values when serialized to JSON.
     /// If `T implements `DeserializeOwned`, prefer using `assert_allows_ser_roundtrip()`
     pub fn assert_allows_ser_only(&self, samples: impl IntoIterator<Item = T>) -> &Self {
@@ -181,7 +187,7 @@ impl<T: JsonSchema + Serialize> TestHelper<T> {
     }
 }
 
-impl<T: JsonSchema + Serialize + DeserializeOwned> TestHelper<T> {
+impl<T: Serialize + DeserializeOwned> TestHelper<T> {
     /// Checks that the "serialize" schema allows the given sample values when serialized to JSON
     /// and, if the value can then be deserialized, that the "deserialize" schema also allows it.
     pub fn assert_allows_ser_roundtrip(&self, samples: impl IntoIterator<Item = T>) -> &Self {