use crate::prelude::*; use pretty_assertions::assert_eq; use schemars::generate::SchemaSettings; use std::collections::BTreeMap; #[derive(JsonSchema, Deserialize, Serialize)] struct UnitStruct; #[derive(JsonSchema, Deserialize, Serialize, Default)] struct Struct { foo: i32, bar: bool, } #[derive(JsonSchema, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] enum External { UnitOne, StringMap(BTreeMap), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { foo: i32, bar: bool, }, EmptyTuple(), Tuple(i32, bool), UnitTwo, #[serde(with = "unit_variant_as_u64")] #[schemars(with = "u64")] UnitAsInt, #[serde(with = "tuple_variant_as_str")] #[schemars(schema_with = "tuple_variant_as_str::json_schema")] TupleAsStr(i32, bool), } impl External { fn values() -> impl IntoIterator { [ Self::UnitOne, Self::StringMap( [("hello".to_owned(), "world".to_owned())] .into_iter() .collect(), ), Self::UnitStructNewType(UnitStruct), Self::StructNewType(Struct { foo: 123, bar: true, }), Self::Struct { foo: 123, bar: true, }, // Ignored due to https://github.com/serde-rs/json/issues/1084 // Self::EmptyTuple(), Self::Tuple(456, false), Self::UnitTwo, Self::UnitAsInt, Self::TupleAsStr(789, true), ] } } #[derive(JsonSchema, Deserialize, Serialize)] #[serde(tag = "tag")] enum Internal { UnitOne, StringMap(BTreeMap), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { foo: i32, bar: bool }, // Internally-tagged enums don't support tuple variants // EmptyTuple(), // Tuple(i32, bool), UnitTwo, // Internally-tagged enum variants don't support non-object "payloads" // #[serde(with = "unit_variant_as_u64")] // #[schemars(with = "u64")] // UnitAsInt, // Internally-tagged enums don't support tuple variants // #[serde(with = "tuple_variant_as_str")] // #[schemars(schema_with = "tuple_variant_as_str::json_schema")] // TupleAsStr(i32, bool), } impl Internal { fn values() -> impl IntoIterator { [ Self::UnitOne, Self::StringMap( [("hello".to_owned(), "world".to_owned())] .into_iter() .collect(), ), Self::UnitStructNewType(UnitStruct), Self::StructNewType(Struct { foo: 123, bar: true, }), Self::Struct { foo: 123, bar: true, }, // Self::EmptyTuple(), // Self::Tuple(456, false), Self::UnitTwo, // Self::UnitAsInt, // Self::TupleAsStr(789, true), ] } } #[derive(JsonSchema, Deserialize, Serialize)] #[serde(tag = "tag", content = "content")] enum Adjacent { UnitOne, StringMap(BTreeMap), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { foo: i32, bar: bool, }, EmptyTuple(), Tuple(i32, bool), UnitTwo, #[serde(with = "unit_variant_as_u64")] #[schemars(with = "u64")] UnitAsInt, #[serde(with = "tuple_variant_as_str")] #[schemars(schema_with = "tuple_variant_as_str::json_schema")] TupleAsStr(i32, bool), } impl Adjacent { fn values() -> impl IntoIterator { [ Self::UnitOne, Self::StringMap( [("hello".to_owned(), "world".to_owned())] .into_iter() .collect(), ), Self::UnitStructNewType(UnitStruct), Self::StructNewType(Struct { foo: 123, bar: true, }), Self::Struct { foo: 123, bar: true, }, Self::EmptyTuple(), Self::Tuple(456, false), Self::UnitTwo, Self::UnitAsInt, Self::TupleAsStr(789, true), ] } } #[derive(JsonSchema, Deserialize, Serialize)] #[serde(untagged)] enum Untagged { UnitOne, StringMap(BTreeMap), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { foo: i32, bar: bool, }, EmptyTuple(), Tuple(i32, bool), UnitTwo, #[serde(with = "unit_variant_as_u64")] #[schemars(with = "u64")] UnitAsInt, #[serde(with = "tuple_variant_as_str")] #[schemars(schema_with = "tuple_variant_as_str::json_schema")] TupleAsStr(i32, bool), } impl Untagged { fn values() -> impl IntoIterator { [ Self::UnitOne, Self::StringMap( [("hello".to_owned(), "world".to_owned())] .into_iter() .collect(), ), Self::UnitStructNewType(UnitStruct), Self::StructNewType(Struct { foo: 123, bar: true, }), Self::Struct { foo: 123, bar: true, }, Self::EmptyTuple(), Self::Tuple(456, false), Self::UnitTwo, Self::UnitAsInt, Self::TupleAsStr(789, true), ] } } mod unit_variant_as_u64 { pub(super) fn serialize(ser: S) -> Result where S: serde::Serializer, { ser.serialize_u64(42) } pub(super) fn deserialize<'de, D>(deser: D) -> Result<(), D::Error> where D: serde::Deserializer<'de>, { use serde::de::Deserialize; u64::deserialize(deser).map(|_| ()) } } mod tuple_variant_as_str { pub(super) fn serialize(i: &i32, b: &bool, ser: S) -> Result where S: serde::Serializer, { ser.collect_str(&format_args!("{i} {b}")) } pub(super) fn deserialize<'de, D>(deser: D) -> Result<(i32, bool), D::Error> where D: serde::Deserializer<'de>, { use serde::de::{Deserialize, Error}; let error = || Error::custom("invalid string"); let (i, b) = <&str>::deserialize(deser)? .split_once(' ') .ok_or_else(error)?; Ok(( i.parse().map_err(|_| error())?, b.parse().map_err(|_| error())?, )) } pub(super) fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { schemars::json_schema!({ "type": "string", "pattern": r"^\d+ (true|false)$" }) } } #[test] fn externally_tagged_enum() { test!(External) .assert_snapshot() .assert_allows_ser_roundtrip(External::values()) .assert_matches_de_roundtrip(arbitrary_values()); } #[test] fn internally_tagged_enum() { test!(Internal) .assert_snapshot() .assert_allows_ser_roundtrip(Internal::values()) .assert_matches_de_roundtrip(arbitrary_values()); } #[test] fn adjacently_tagged_enum() { test!(Adjacent) .assert_snapshot() .assert_allows_ser_roundtrip(Adjacent::values()) .assert_matches_de_roundtrip(arbitrary_values()); } #[test] fn untagged_enum() { test!(Untagged) .assert_snapshot() .assert_allows_ser_roundtrip(Untagged::values()) .assert_matches_de_roundtrip(arbitrary_values()); } #[test] fn untagged_enum_with_titles() { test!( Untagged, SchemaSettings::default().with(|s| s.untagged_enum_variant_titles = true) ) .custom(|s, _| { assert_eq!( s.pointer("/anyOf/0/title").and_then(Value::as_str), Some("UnitOne") ); for variant in s.pointer("/anyOf").and_then(Value::as_array).unwrap() { assert!(variant.get("title").is_some_and(Value::is_string)); } }) .assert_snapshot() .assert_allows_ser_roundtrip(Untagged::values()) .assert_matches_de_roundtrip(arbitrary_values()); } #[derive(JsonSchema, Serialize, Deserialize)] enum NoVariants {} #[test] fn no_variants() { test!(NoVariants) .assert_snapshot() .assert_rejects_de(arbitrary_values()); } #[derive(JsonSchema, Serialize, Deserialize)] #[serde(rename_all_fields = "UPPERCASE", rename_all = "snake_case")] enum Renamed { StructVariant { field: String, }, #[serde(rename = "custom name variant")] RenamedStructVariant { #[serde(rename = "custom name field")] field: String, }, } #[test] fn renamed() { test!(Renamed) .assert_snapshot() .assert_allows_ser_roundtrip([ Renamed::StructVariant { field: "foo".to_owned(), }, Renamed::RenamedStructVariant { field: "bar".to_owned(), }, ]) .assert_rejects_de(arbitrary_values()); } #[allow(dead_code)] #[derive(JsonSchema, Deserialize, Serialize)] enum SoundOfMusic { /// # A deer /// /// A female deer Do, /// A drop of golden sun Re, /// A name I call myself Mi, } #[test] fn unit_variants_with_doc_comments() { test!(SoundOfMusic) .assert_snapshot() .assert_allows_ser_roundtrip([SoundOfMusic::Do, SoundOfMusic::Re, SoundOfMusic::Mi]) .assert_rejects_de(arbitrary_values()) .custom(|schema, _| { assert_eq!( schema.as_value().pointer("/oneOf/0/title"), Some(&("A deer".into())), ); assert_eq!( schema.as_value().pointer("/oneOf/0/description"), Some(&("A female deer".into())), ); assert_eq!( schema.as_value().pointer("/oneOf/1/description"), Some(&("A drop of golden sun".into())), ); assert_eq!( schema.as_value().pointer("/oneOf/2/description"), Some(&("A name I call myself".into())), ); }); }