//! Test Cases use crate::utils::{check_matches_schema, check_valid_json_schema}; use expect_test::expect_file; use schemars::JsonSchema; use serde::Serialize; use serde_json::json; use serde_with::*; use std::collections::BTreeSet; // This avoids us having to add `#[schemars(crate = "::schemars_0_8")]` all // over the place. We're not testing that and it is inconvenient. extern crate schemars_0_8 as schemars; mod utils; /// Declare a snapshot tests for a struct. /// /// The snapshot files are stored under the `schemars_0_8` folder alongside /// this test file. macro_rules! declare_snapshot_test { {$( $( #[$tattr:meta] )* $test:ident { $( #[$stattr:meta] )* struct $name:ident { $( $( #[ $fattr:meta ] )* $field:ident : $ty:ty ),* $(,)? } } )*} => {$( #[test] $(#[$tattr])* fn $test() { #[serde_as] #[derive(JsonSchema, Serialize)] $( #[$stattr] )* struct $name { $( $( #[$fattr] )* $field: $ty, )* } let schema = schemars::schema_for!($name); let mut schema = serde_json::to_string_pretty(&schema) .expect("schema could not be serialized"); schema.push('\n'); let filename = concat!("./", module_path!(), "::", stringify!($test), ".json") .replace("::", "/"); let expected = expect_file![filename]; expected.assert_eq(&schema); } )*} } #[test] fn schemars_basic() { #[serde_as] #[derive(JsonSchema, Serialize)] #[schemars(crate = "::schemars_0_8")] struct Basic { /// Basic field, no attribute bare_field: u32, /// Field that directly uses DisplayFromStr #[serde_as(as = "DisplayFromStr")] display_from_str: u32, /// Same does not implement JsonSchema directly so this checks that the /// correct schemars attribute was injected. #[serde_as(as = "Same")] same: u32, /// This checks that Same still works when wrapped in a box. #[serde_as(as = "Box")] box_same: Box, /// Same thing, but with a Vec this time. #[serde_as(as = "Vec<_>")] vec_same: Vec, } let schema = schemars::schema_for!(Basic); let mut schema = serde_json::to_string_pretty(&schema).expect("schema could not be serialized"); schema.push('\n'); let expected = expect_file!["./schemars_0_8/schemars_basic.json"]; expected.assert_eq(&schema); } #[test] fn schemars_other_cfg_attrs() { #[serde_as] #[derive(JsonSchema, Serialize)] struct Test { #[serde_as(as = "DisplayFromStr")] #[cfg_attr(any(), arbitrary("some" |weird| syntax::()))] #[cfg_attr(any(), schemars(with = "i32"))] custom: i32, } check_matches_schema::(&json!({ "custom": "23", })); } #[test] fn schemars_custom_with() { #[serde_as] #[derive(JsonSchema, Serialize)] struct Test { #[serde_as(as = "DisplayFromStr")] #[schemars(with = "i32")] custom: i32, #[serde_as(as = "DisplayFromStr")] #[cfg_attr(any(), schemars(with = "i32"))] with_disabled: i32, #[serde_as(as = "DisplayFromStr")] #[cfg_attr(all(), schemars(with = "i32"))] always_enabled: i32, } check_matches_schema::(&json!({ "custom": 3, "with_disabled": "5", "always_enabled": 7, })); } #[test] fn schemars_deserialize_only_bug_735() { #[serde_as] #[derive(JsonSchema, Serialize)] #[schemars(crate = "::schemars_0_8")] struct Basic { /// Basic field, no attribute bare_field: u32, /// Will emit matching schemars attribute #[serde_as(as = "PickFirst<(_, DisplayFromStr)>")] both: u32, /// Can emit schemars with serialize_as, but it will be ignored #[serde_as(serialize_as = "PickFirst<(_, DisplayFromStr)>")] serialize_only: u32, /// schemars doesn't support deserialize_as #[serde_as(deserialize_as = "PickFirst<(_, DisplayFromStr)>")] deserialize_only: u32, /// Can emit schemars with serialize_as, but it will be ignored /// schemars doesn't support deserialize_as #[serde_as( serialize_as = "PickFirst<(_, DisplayFromStr)>", deserialize_as = "PickFirst<(_, DisplayFromStr)>" )] serialize_and_deserialize: u32, } let schema = schemars::schema_for!(Basic); let mut schema = serde_json::to_string_pretty(&schema).expect("schema could not be serialized"); schema.push('\n'); let expected = expect_file!["./schemars_0_8/schemars_deserialize_only_bug_735.json"]; expected.assert_eq(&schema); } #[test] fn schemars_custom_schema_with() { fn custom_int(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { use schemars::schema::*; SchemaObject { instance_type: Some(InstanceType::Integer.into()), ..Default::default() } .into() } #[serde_as] #[derive(JsonSchema, Serialize)] struct Test { #[serde_as(as = "DisplayFromStr")] #[schemars(schema_with = "custom_int")] custom: i32, #[serde_as(as = "DisplayFromStr")] #[cfg_attr(any(), schemars(schema_with = "custom_int"))] with_disabled: i32, #[serde_as(as = "DisplayFromStr")] #[cfg_attr(all(), schemars(schema_with = "custom_int"))] always_enabled: i32, } check_matches_schema::(&json!({ "custom": 3, "with_disabled": "5", "always_enabled": 7, })); } mod test_std { use super::*; use std::collections::{BTreeMap, VecDeque}; declare_snapshot_test! { option { struct Test { #[serde_with(as = "Option<_>")] optional: Option, } } vec { struct Test { #[serde_with(as = "Vec<_>")] vec: Vec } } vec_deque { struct Test { #[serde_with(as = "VecDeque<_>")] vec_deque: VecDeque } } map { struct Test { #[serde_with(as = "BTreeMap<_, _>")] map: BTreeMap } } set { struct Test { #[serde_with(as = "BTreeSet<_>")] map: BTreeSet, } } tuples { struct Test { #[serde_with(as = "()")] tuple0: (), #[serde_with(as = "(_ ,)")] tuple1: (i32,), #[serde_with(as = "(_, _)")] tuple2: (i32, i32), #[serde_with(as = "(_, _, _)")] tuple3: (i32, i32, String) } } } } mod snapshots { use super::*; use serde_with::formats::*; #[allow(dead_code)] #[derive(JsonSchema, Serialize)] enum Mappable { A(i32), B(String), C { c: i32, b: Option }, } #[derive(JsonSchema, Serialize)] struct KvMapData { #[serde(rename = "$key$")] key: String, a: u32, b: String, c: f32, d: bool, } #[allow(dead_code, variant_size_differences)] #[derive(JsonSchema, Serialize)] #[serde(tag = "$key$")] enum KvMapEnum { TypeA { a: u32 }, TypeB { b: String }, TypeC { c: bool }, } #[derive(JsonSchema, Serialize)] struct KvMapFlatten { #[serde(flatten)] data: KvMapEnum, extra: bool, } declare_snapshot_test! { bytes { struct Test { #[serde_as(as = "Bytes")] bytes: Vec, } } default_on_null { struct Test { #[serde_as(as = "DefaultOnNull<_>")] data: String, } } string_with_separator { struct Test { #[serde_as(as = "StringWithSeparator")] data: Vec, } } from_into { struct Test { #[serde_as(as = "FromInto")] data: u32, } } map { struct Test { #[serde_as(as = "Map<_, _>")] data: Vec<(String, u32)>, } } map_fixed { struct Test { #[serde_as(as = "Map<_, _>")] data: [(String, u32); 4], } } set_last_value_wins { struct Test { #[serde_as(as = "SetLastValueWins<_>")] data: BTreeSet, } } set_prevent_duplicates { struct Test { #[serde_as(as = "SetPreventDuplicates<_>")] data: BTreeSet, } } duration { struct Test { #[serde_as(as = "DurationSeconds")] seconds: std::time::Duration, #[serde_as(as = "DurationSecondsWithFrac")] frac: std::time::Duration, #[serde_as(as = "DurationSeconds")] flexible_string: std::time::Duration, #[serde_as(as = "DurationSeconds")] seconds_u64_strict: std::time::Duration, #[serde_as(as = "TimestampSeconds")] time_i64: std::time::SystemTime, } } enum_map { struct Test { #[serde_as(as = "EnumMap")] data: Vec, } } key_value_map { struct Test { #[serde_as(as = "KeyValueMap<_>")] data: Vec, } } key_value_map_enum { struct Test { #[serde_as(as = "KeyValueMap<_>")] data: Vec, } } key_value_map_flatten { struct Test { #[serde_as(as = "KeyValueMap<_>")] data: Vec, } } one_or_many_prefer_one { #[serde(transparent)] struct Test { #[serde_as(as = "OneOrMany<_, PreferOne>")] data: Vec, } } pickfirst { #[serde(transparent)] struct Test { #[serde_as(as = "PickFirst<(_, DisplayFromStr)>")] value: u32 } } one_or_many_nested { struct Test { #[serde_as(as = "Option>")] optional_many: Option>, } } } } mod derive { use super::*; #[serde_as] #[derive(Serialize)] #[cfg_attr(all(), derive(JsonSchema))] struct Enabled { #[serde_as(as = "DisplayFromStr")] field: u32, } #[allow(dead_code)] #[serde_as] #[derive(Serialize)] #[cfg_attr(any(), derive(JsonSchema))] struct Disabled { // If we are incorrectly adding `#[schemars(with = ...)]` attributes // then we should get an error on this field. #[serde_as(as = "DisplayFromStr")] field: u32, } #[test] fn test_enabled_has_correct_schema() { check_valid_json_schema(&Enabled { field: 77 }); } } mod array { use super::*; #[serde_as] #[derive(JsonSchema, Serialize)] struct FixedArray { #[serde_as(as = "[_; 3]")] array: [u32; 3], } #[test] fn test_serialized_is_valid() { let array = FixedArray { array: [1, 2, 3] }; check_valid_json_schema(&array); } #[test] fn test_valid_json() { let value = json!({ "array": [1, 2, 3] }); check_matches_schema::(&value); } #[test] #[should_panic] fn test_too_short() { check_matches_schema::(&json!({ "array": [1], })); } #[test] #[should_panic] fn test_too_long() { check_matches_schema::(&json!({ "array": [1, 2, 3, 4] })); } #[test] #[should_panic] fn test_wrong_item_type() { check_matches_schema::(&json!({ "array": ["1", "2", "3"] })); } #[test] #[should_panic] fn test_oob_item() { check_matches_schema::(&json!({ "array": [-1, 0x1_0000_0000i64, 32] })); } } mod bool_from_int { use super::*; use serde_with::formats::{Flexible, Strict}; #[serde_as] #[derive(Serialize, JsonSchema)] struct BoolStrict { #[serde_as(as = "BoolFromInt")] value: bool, } #[serde_as] #[derive(Serialize, JsonSchema)] struct BoolFlexible { #[serde_as(as = "BoolFromInt")] value: bool, } #[test] fn test_serialized_strict_is_valid() { check_valid_json_schema(&vec![ BoolStrict { value: true }, BoolStrict { value: false }, ]); } #[test] fn test_serialized_flexible_is_valid() { check_valid_json_schema(&vec![ BoolFlexible { value: true }, BoolFlexible { value: false }, ]); } #[test] #[should_panic] fn strict_out_of_range() { check_matches_schema::(&json!({ "value": 5 })); } #[test] fn flexible_out_of_range() { check_matches_schema::(&json!({ "value": 5 })); } #[test] #[should_panic] fn flexible_wrong_type() { check_matches_schema::(&json!({ "value": "seven" })); } #[test] #[should_panic] fn test_fractional_value_strict() { check_matches_schema::(&json!({ "value": 0.5 })); } #[test] #[should_panic] fn test_fractional_value_flexible() { check_matches_schema::(&json!({ "value": 0.5 })); } } mod bytes_or_string { use super::*; #[serde_as] #[derive(Serialize, JsonSchema)] struct Test { #[serde_as(as = "BytesOrString")] bytes: Vec, } #[test] fn test_serialized_is_valid() { check_valid_json_schema(&Test { bytes: b"test".to_vec(), }); } #[test] fn test_string_valid_json() { check_matches_schema::(&json!({ "bytes": "test string" })); } #[test] fn test_bytes_valid_json() { check_matches_schema::(&json!({ "bytes": [1, 2, 3, 4] })); } #[test] #[should_panic] fn test_int_not_valid_json() { check_matches_schema::(&json!({ "bytes": 5 })); } } mod enum_map { use super::*; #[derive(Serialize, JsonSchema)] struct InnerStruct { c: String, d: f64, } #[derive(Serialize, JsonSchema)] enum Inner { A(i32), B(String), C(InnerStruct), } #[serde_as] #[derive(Serialize, JsonSchema)] #[serde(transparent)] struct Outer(#[serde_as(as = "EnumMap")] Vec); #[test] fn test_serialized_is_valid() { check_valid_json_schema(&Outer(vec![ Inner::A(5), Inner::B("test".into()), Inner::C(InnerStruct { c: "c".into(), d: -34.0, }), ])); } #[test] fn test_matches_expected() { check_matches_schema::(&json!({ "A": 75, "B": "BBBBBB", "C": { "c": "inner C", "d": 777 } })); } #[test] fn test_no_fields_required() { check_matches_schema::(&json!({})); } #[test] #[should_panic] fn test_mixed_up_schemas() { check_matches_schema::(&json!({ "A": "b", "B": 5 })); } #[test] #[should_panic] fn test_invalid_key() { check_matches_schema::(&json!({ "invalid": 4 })); } } mod duration { use super::*; use serde_with::formats::{Flexible, Strict}; use std::time::{Duration, SystemTime}; #[serde_as] #[derive(Serialize, JsonSchema)] struct DurationTest { #[serde_as(as = "DurationSeconds")] strict_u64: Duration, #[serde_as(as = "DurationSeconds")] strict_str: Duration, #[serde_as(as = "DurationSecondsWithFrac")] strict_f64: Duration, #[serde_as(as = "DurationSeconds")] flexible_u64: Duration, #[serde_as(as = "DurationSeconds")] flexible_f64: Duration, #[serde_as(as = "DurationSeconds")] flexible_str: Duration, } #[test] fn test_serialized_is_valid() { check_valid_json_schema(&DurationTest { strict_u64: Duration::from_millis(2500), strict_str: Duration::from_millis(2500), strict_f64: Duration::from_millis(2500), flexible_u64: Duration::from_millis(2500), flexible_f64: Duration::from_millis(2500), flexible_str: Duration::from_millis(2500), }); } #[serde_as] #[derive(Serialize, JsonSchema)] struct FlexibleU64Duration(#[serde_as(as = "DurationSeconds")] Duration); #[serde_as] #[derive(Serialize, JsonSchema)] struct FlexibleStringDuration(#[serde_as(as = "DurationSeconds")] Duration); #[serde_as] #[derive(Serialize, JsonSchema)] struct FlexibleTimestamp(#[serde_as(as = "TimestampSeconds")] SystemTime); #[test] fn test_string_as_flexible_u64() { check_matches_schema::(&json!("32")); } #[test] fn test_integer_as_flexible_u64() { check_matches_schema::(&json!(16)); } #[test] fn test_number_as_flexible_u64() { check_matches_schema::(&json!(54.1)); } #[test] #[should_panic] fn test_negative_as_flexible_u64() { check_matches_schema::(&json!(-5)); } #[test] fn test_string_as_flexible_string() { check_matches_schema::(&json!("32")); } #[test] fn test_integer_as_flexible_string() { check_matches_schema::(&json!(16)); } #[test] fn test_number_as_flexible_string() { check_matches_schema::(&json!(54.1)); } #[test] #[should_panic] fn test_negative_as_flexible_string() { check_matches_schema::(&json!(-5)); } #[test] fn test_negative_as_flexible_timestamp() { check_matches_schema::(&json!(-50000)); } #[test] fn test_negative_string_as_flexible_timestamp() { check_matches_schema::(&json!("-50000")); } } #[test] fn test_borrow_cow() { use std::borrow::Cow; #[serde_as] #[derive(Serialize, JsonSchema)] struct Borrowed<'a> { #[serde_as(as = "BorrowCow")] data: Cow<'a, str>, } check_valid_json_schema(&Borrowed { data: Cow::Borrowed("test"), }); } #[test] fn test_map() { #[serde_as] #[derive(Serialize, JsonSchema)] struct Test { map: [(&'static str, u32); 2], } check_valid_json_schema(&Test { map: [("a", 1), ("b", 2)], }); } #[test] fn test_if_is_human_readable() { #[serde_as] #[derive(Serialize, JsonSchema)] struct Test { #[serde_as(as = "IfIsHumanReadable")] data: i32, } check_valid_json_schema(&Test { data: 5 }); check_matches_schema::(&json!({ "data": "5" })); } #[test] fn test_set_last_value_wins_with_duplicates() { #[serde_as] #[derive(Serialize, JsonSchema)] struct Test { #[serde_as(as = "SetLastValueWins<_>")] set: BTreeSet, } check_matches_schema::(&json!({ "set": [ 1, 2, 3, 1, 4, 2 ] })); } #[test] #[should_panic] fn test_set_prevent_duplicates_with_duplicates() { #[serde_as] #[derive(Serialize, JsonSchema)] struct Test { #[serde_as(as = "SetPreventDuplicates<_>")] set: BTreeSet, } check_matches_schema::(&json!({ "set": [ 1, 1 ] })); } mod key_value_map { use super::*; use std::collections::BTreeMap; #[serde_as] #[derive(Clone, Debug, JsonSchema, Serialize)] #[serde(transparent)] struct KVMap( #[serde_as(as = "KeyValueMap<_>")] #[serde(bound(serialize = "E: Serialize", deserialize = "E: Deserialize<'de>"))] Vec, ); #[derive(Clone, Debug, JsonSchema, Serialize)] #[serde(untagged)] enum UntaggedEnum { A { #[serde(rename = "$key$")] key: String, field1: String, }, B(String, i32), } #[test] fn test_untagged_enum() { let value = KVMap(vec![ UntaggedEnum::A { key: "v1".into(), field1: "field".into(), }, UntaggedEnum::B("v2".into(), 7), ]); check_valid_json_schema(&value); } #[derive(Clone, Debug, JsonSchema, Serialize)] #[serde(untagged)] enum UntaggedNestedEnum { Nested(UntaggedEnum), C { #[serde(rename = "$key$")] key: String, field2: i32, }, } #[test] fn test_untagged_nested_enum() { let value = KVMap(vec![ UntaggedNestedEnum::Nested(UntaggedEnum::A { key: "v1".into(), field1: "field".into(), }), UntaggedNestedEnum::Nested(UntaggedEnum::B("v2".into(), 7)), UntaggedNestedEnum::C { key: "v2".into(), field2: 222, }, ]); check_valid_json_schema(&value); } #[test] fn test_btreemap() { let value = KVMap(vec![ BTreeMap::from_iter([("$key$", "a"), ("value", "b")]), BTreeMap::from_iter([("$key$", "b"), ("value", "d")]), ]); check_valid_json_schema(&value); } } mod one_or_many { use super::*; use serde_with::formats::{PreferMany, PreferOne}; #[serde_as] #[derive(Clone, Debug, JsonSchema, Serialize)] #[serde(transparent)] struct WithPreferOne(#[serde_as(as = "OneOrMany<_, PreferOne>")] Vec); #[serde_as] #[derive(Clone, Debug, JsonSchema, Serialize)] #[serde(transparent)] struct WithPreferMany(#[serde_as(as = "OneOrMany<_, PreferMany>")] Vec); #[test] fn test_prefer_one() { let single = WithPreferOne(vec![7]); let multiple = WithPreferOne(vec![1, 2, 3]); check_valid_json_schema(&single); check_valid_json_schema(&multiple); } #[test] fn test_prefer_one_matches() { check_matches_schema::(&json!(7)); check_matches_schema::(&json!([1, 2, 3])); } #[test] #[should_panic] fn test_prefer_one_no_invalid_type_one() { check_matches_schema::(&json!("test")); } #[test] #[should_panic] fn test_prefer_one_no_invalid_type_many() { check_matches_schema::(&json!(["test", 1])); } #[test] fn test_prefer_many() { let single = WithPreferMany(vec![7]); let multiple = WithPreferMany(vec![1, 2, 3]); check_valid_json_schema(&single); check_valid_json_schema(&multiple); } #[test] #[should_panic] fn test_prefer_many_no_invalid_type_one() { check_matches_schema::(&json!("test")); } #[test] #[should_panic] fn test_prefer_many_no_invalid_type_many() { check_matches_schema::(&json!(["test", 1])); } } #[test] fn test_pickfirst() { #[serde_as] #[derive(JsonSchema, Serialize)] #[serde(transparent)] struct IntOrDisplay(#[serde_as(as = "PickFirst<(_, DisplayFromStr)>")] u32); check_matches_schema::(&json!(7)); check_matches_schema::(&json!("17")); }