#![cfg(all(feature = "macros", feature = "test-util"))]
use std::collections::BTreeMap;
use kynos::{
Schema,
openapi::{
Schema as OpenApiSchema, SchemaObject,
model::schema::types::{SchemaType, TypeSet},
},
schema::{
MapKey, Schema as SchemaTrait, constraints::Constraints, flatten::OpenMap,
registry::Registry,
},
};
use serde::Serialize;
fn emitted<T: SchemaTrait>() -> serde_json::Value {
let mut registry = kynos::schema::registry::Registry::new();
let body = T::schema(&mut registry);
let mut root = serde_json::to_value(body).expect("a schema serializes");
let components =
serde_json::to_value(registry.into_components()).expect("the components serialize");
root.as_object_mut()
.expect("a derived schema is an object")
.insert("components".to_owned(), components);
root
}
fn refusals<T: SchemaTrait + Serialize>(value: &T) -> Vec<String> {
let schema = emitted::<T>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
let instance = serde_json::to_value(value).expect("the value serializes");
validator
.iter_errors(&instance)
.map(|error| error.to_string())
.collect()
}
#[derive(Schema, Serialize, serde::Deserialize)]
struct Audit {
at: String,
}
#[derive(Schema, Serialize)]
struct Audited {
id: u64,
#[serde(flatten)]
audit: Audit,
}
#[derive(Schema, Serialize)]
struct Thing {
id: u64,
#[serde(flatten)]
#[schema(open)]
extra: BTreeMap<String, String>,
}
fn audited() -> Audited {
Audited {
id: 1,
audit: Audit {
at: "2026-01-01T00:00:00Z".to_owned(),
},
}
}
fn thing() -> Thing {
Thing {
id: 1,
extra: BTreeMap::from([("k".to_owned(), "v".to_owned())]),
}
}
#[test]
fn a_flattened_struct_leaves_the_parents_own_properties_alone() {
let refusals = refusals(&audited());
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Audited>()
);
}
#[derive(Schema, Serialize)]
#[serde(tag = "type")]
struct StampedAudit {
at: String,
}
#[derive(Schema, Serialize)]
struct Carrier {
id: u64,
#[serde(flatten)]
stamp: StampedAudit,
}
#[test]
fn a_flattened_tagged_struct_holds_its_tag_through_the_parent() {
let carrier = Carrier {
id: 1,
stamp: StampedAudit {
at: "2026-01-01T00:00:00Z".to_owned(),
},
};
let refusals = refusals(&carrier);
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Carrier>()
);
let schema = emitted::<Carrier>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
for document in [
serde_json::json!({ "id": 1, "at": "t", "type": "Other" }),
serde_json::json!({ "id": 1, "at": "t" }),
] {
assert!(
!validator.is_valid(&document),
"a tag serde never writes was accepted: {document} against {schema}"
);
}
}
#[test]
fn an_open_flattened_map_leaves_the_parents_own_properties_alone() {
let refusals = refusals(&thing());
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Thing>()
);
}
#[test]
fn an_open_flattened_map_still_constrains_the_members_it_contributes() {
let schema = emitted::<Thing>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
assert!(
!validator.is_valid(&serde_json::json!({ "id": 1, "k": 2 })),
"a member contributed by a `BTreeMap<String, String>` was accepted as a number: {schema}"
);
}
#[test]
fn an_open_flattened_map_hoists_its_values_to_unevaluated_properties() {
let schema = emitted::<Thing>();
assert_eq!(
schema["unevaluatedProperties"],
serde_json::json!({ "type": "string" }),
"{schema}"
);
assert!(
schema["allOf"][0]["additionalProperties"].is_null(),
"the map's `additionalProperties` stayed inside the `allOf` branch: {schema}"
);
assert!(
schema["additionalProperties"].is_null(),
"the map's values reached the parent's `additionalProperties`: {schema}"
);
}
#[derive(Schema, Serialize, serde::Deserialize)]
struct Sparse {
id: u64,
#[serde(flatten, skip_serializing_if = "BTreeMap::is_empty")]
#[schema(open)]
extra: BTreeMap<String, String>,
}
#[test]
fn an_open_map_that_skips_itself_when_empty_agrees_with_its_schema() {
let empty = Sparse {
id: 1,
extra: BTreeMap::new(),
};
let written = serde_json::to_value(&empty).expect("the value serializes");
assert_eq!(
written,
serde_json::json!({ "id": 1 }),
"an empty map reached the object serde wrote"
);
let omitted = refusals(&empty);
assert!(
omitted.is_empty(),
"the object written without the map fails its own description: {omitted:?}\n\
schema: {}",
emitted::<Sparse>()
);
let read: Sparse = serde_json::from_value(written).expect("the object serde wrote reads back");
assert!(
read.extra.is_empty(),
"an absent map read back with members: {:?}",
read.extra
);
let populated = refusals(&Sparse {
id: 1,
extra: BTreeMap::from([("k".to_owned(), "v".to_owned())]),
});
assert!(
populated.is_empty(),
"a map with members fails its own description: {populated:?}\n\
schema: {}",
emitted::<Sparse>()
);
let schema = emitted::<Sparse>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
assert!(
!validator.is_valid(&serde_json::json!({ "id": 1, "k": 2 })),
"a member contributed by a `BTreeMap<String, String>` was accepted as a number: {schema}"
);
}
#[derive(Schema, Serialize)]
struct Both {
id: u64,
#[serde(flatten)]
audit: Audit,
#[serde(flatten)]
#[schema(open)]
extra: BTreeMap<String, String>,
}
fn both() -> Both {
Both {
id: 1,
audit: Audit {
at: "2026-01-01T00:00:00Z".to_owned(),
},
extra: BTreeMap::from([("k".to_owned(), "v".to_owned())]),
}
}
#[test]
fn an_open_map_beside_a_flattened_struct_accepts_what_each_contributes() {
let refusals = refusals(&both());
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Both>()
);
}
#[test]
fn an_open_map_beside_a_flattened_struct_weakens_neither() {
let schema = emitted::<Both>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
assert!(
!validator.is_valid(&serde_json::json!({
"id": 1, "at": "2026-01-01T00:00:00Z", "k": 2
})),
"a member contributed by a `BTreeMap<String, String>` was accepted as a number: {schema}"
);
assert!(
!validator.is_valid(&serde_json::json!({ "id": 1, "k": "v" })),
"the flattened struct's own required property was not required: {schema}"
);
}
#[derive(Schema, Serialize, PartialEq, Eq, PartialOrd, Ord)]
struct Sku(String);
impl MapKey for Sku {
fn key_constraints() -> Constraints {
let mut constraints = Constraints::default();
constraints.pattern = Some("^[A-Z]{3}-[0-9]{4}$".to_owned());
constraints
}
}
#[derive(Schema, Serialize)]
struct Ledger {
counts: BTreeMap<Sku, u64>,
}
#[derive(Schema, Serialize)]
struct Stock {
id: u64,
#[serde(flatten)]
#[schema(open)]
counts: BTreeMap<Sku, u64>,
}
#[test]
fn an_open_flatten_drops_the_key_constraint_rather_than_reaching_the_parent() {
assert!(
emitted::<Ledger>().to_string().contains("propertyNames"),
"a map keyed by `Sku` emitted no key constraint at all: {}",
emitted::<Ledger>()
);
let stock = Stock {
id: 1,
counts: BTreeMap::from([(Sku("ABC-0001".to_owned()), 3)]),
};
let schema = emitted::<Stock>();
assert!(
!schema.to_string().contains("propertyNames"),
"a key constraint survived an open flatten: {schema}"
);
let refusals = refusals(&stock);
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {schema}"
);
}
#[derive(Schema, Serialize)]
#[serde(tag = "kind")]
enum Tally {
Counted {
id: u64,
#[serde(flatten)]
#[schema(open)]
extra: BTreeMap<String, u64>,
},
}
#[test]
fn an_open_map_in_a_tagged_variant_leaves_the_tag_and_the_variants_properties_alone() {
let tally = Tally::Counted {
id: 1,
extra: BTreeMap::from([("k".to_owned(), 2)]),
};
let refusals = refusals(&tally);
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Tally>()
);
let schema = emitted::<Tally>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
assert!(
!validator.is_valid(&serde_json::json!({ "kind": "Counted", "id": 1, "k": "v" })),
"a member contributed by a `BTreeMap<String, u64>` was accepted as a string: {schema}"
);
}
#[derive(Schema, Serialize)]
#[serde(tag = "kind", content = "data")]
enum Wrapped {
Counted {
id: u64,
name: String,
#[serde(flatten)]
#[schema(open)]
extra: BTreeMap<String, u64>,
},
}
#[test]
fn an_open_map_in_an_adjacently_tagged_variant_leaves_the_variants_properties_alone() {
let wrapped = Wrapped::Counted {
id: 1,
name: "n".to_owned(),
extra: BTreeMap::from([("k".to_owned(), 2)]),
};
let refusals = refusals(&wrapped);
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Wrapped>()
);
let schema = emitted::<Wrapped>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
assert!(
!validator.is_valid(&serde_json::json!({
"kind": "Counted", "data": { "id": 1, "name": "n", "k": "v" }
})),
"a member contributed by a `BTreeMap<String, u64>` was accepted as a string: {schema}"
);
}
#[derive(Serialize)]
struct Headers(BTreeMap<String, String>);
impl SchemaTrait for Headers {
fn schema(_registry: &mut Registry) -> OpenApiSchema {
OpenApiSchema::Object(Box::new(SchemaObject {
ty: Some(TypeSet::One(SchemaType::Object)),
additional_properties: Some(Box::new(OpenApiSchema::of_type(SchemaType::String))),
..SchemaObject::default()
}))
}
}
impl OpenMap for Headers {}
#[derive(Schema, Serialize)]
struct Request {
id: u64,
#[serde(flatten)]
#[schema(open)]
headers: Headers,
}
#[test]
fn a_hand_written_open_map_is_hoisted_like_a_standard_one() {
assert!(<Headers as SchemaTrait>::name().is_none());
let request = Request {
id: 1,
headers: Headers(BTreeMap::from([("k".to_owned(), "v".to_owned())])),
};
let refusals = refusals(&request);
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Request>()
);
let schema = emitted::<Request>();
assert_eq!(
schema["unevaluatedProperties"],
serde_json::json!({ "type": "string" }),
"{schema}"
);
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
assert!(
!validator.is_valid(&serde_json::json!({ "id": 1, "k": 2 })),
"a member contributed by `Headers` was accepted as a number: {schema}"
);
}
#[derive(Schema, Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Sealed {
id: u64,
#[serde(flatten)]
audit: Audit,
}
#[test]
fn a_closed_object_admits_what_a_flattened_struct_contributes_and_nothing_else() {
let sealed = Sealed {
id: 1,
audit: Audit {
at: "2026-01-01T00:00:00Z".to_owned(),
},
};
let refusals = refusals(&sealed);
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Sealed>()
);
let schema = emitted::<Sealed>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
let unknown = serde_json::json!({ "id": 1, "at": "t", "z": 2 });
assert!(
serde_json::from_value::<Sealed>(unknown.clone()).is_err(),
"serde must refuse a member no field names"
);
assert!(
!validator.is_valid(&unknown),
"a member serde refuses was accepted: {schema}"
);
}
#[derive(Schema, Serialize, serde::Deserialize)]
struct Located {
#[serde(alias = "place")]
at: String,
}
#[derive(Schema, Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct SealedLocation {
id: u64,
#[serde(flatten)]
location: Located,
}
#[test]
fn a_flattened_structs_alias_is_read_through_the_closed_parent() {
let schema = emitted::<SealedLocation>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
for document in [
serde_json::json!({ "id": 1, "at": "a" }),
serde_json::json!({ "id": 1, "place": "a" }),
] {
assert!(
serde_json::from_value::<SealedLocation>(document.clone()).is_ok(),
"serde must read {document}"
);
assert!(
validator.is_valid(&document),
"a document serde reads was refused: {document}\nschema: {schema}"
);
}
for document in [
serde_json::json!({ "id": 1 }),
serde_json::json!({ "id": 1, "at": "a", "place": "b" }),
serde_json::json!({ "id": 1, "place": "a", "z": 2 }),
] {
assert!(
serde_json::from_value::<SealedLocation>(document.clone()).is_err(),
"serde must refuse {document}"
);
assert!(
!validator.is_valid(&document),
"a document serde refuses was accepted: {document}\nschema: {schema}"
);
}
}
#[derive(Schema, Serialize, serde::Deserialize)]
struct Labelled {
#[serde(alias = "tag")]
label: Option<String>,
}
#[derive(Schema, Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct SealedLabel {
id: u64,
#[serde(flatten)]
labelled: Labelled,
}
#[test]
fn a_flattened_structs_optional_alias_is_read_through_the_closed_parent() {
let schema = emitted::<SealedLabel>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
let written = serde_json::to_value(SealedLabel {
id: 1,
labelled: Labelled { label: None },
})
.expect("the value serializes");
assert_eq!(written, serde_json::json!({ "id": 1, "label": null }));
for document in [
written,
serde_json::json!({ "id": 1 }),
serde_json::json!({ "id": 1, "tag": "a" }),
] {
assert!(
serde_json::from_value::<SealedLabel>(document.clone()).is_ok(),
"serde must read {document}"
);
assert!(
validator.is_valid(&document),
"a document serde reads was refused: {document}\nschema: {schema}"
);
}
let both = serde_json::json!({ "id": 1, "label": "a", "tag": "b" });
assert!(
serde_json::from_value::<SealedLabel>(both.clone()).is_err(),
"serde must refuse {both}"
);
assert!(
!validator.is_valid(&both),
"a document serde refuses was accepted: {both}\nschema: {schema}"
);
}
#[derive(Schema, Serialize, serde::Deserialize)]
#[serde(tag = "kind", content = "value")]
enum Mode {
Fixed { level: u8 },
Off,
}
#[derive(Schema, Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct SealedMode {
id: u64,
#[serde(flatten)]
mode: Mode,
}
#[test]
fn a_closed_object_reads_a_flattened_adjacently_tagged_enum_as_serde_does() {
let schema = emitted::<SealedMode>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
for value in [
SealedMode {
id: 1,
mode: Mode::Fixed { level: 2 },
},
SealedMode {
id: 1,
mode: Mode::Off,
},
] {
let document = serde_json::to_value(&value).expect("the value serializes");
assert!(
serde_json::from_value::<SealedMode>(document.clone()).is_ok(),
"serde must read back what it wrote: {document}"
);
assert!(
validator.is_valid(&document),
"a document serde reads was refused: {document}\nschema: {schema}"
);
}
for document in [
serde_json::json!({ "id": 1, "kind": "Fixed", "value": { "level": 2 }, "z": 3 }),
serde_json::json!({ "id": 1, "value": { "level": 2 } }),
] {
assert!(
serde_json::from_value::<SealedMode>(document.clone()).is_err(),
"serde must refuse {document}"
);
assert!(
!validator.is_valid(&document),
"a document serde refuses was accepted: {document}\nschema: {schema}"
);
}
}
#[derive(Schema, Serialize)]
struct Extended {
#[serde(flatten)]
problem: kynos::Problem,
balance: u32,
}
#[test]
fn a_flattened_problem_accepts_its_extension_members() {
let extended = Extended {
problem: kynos::Problem::new(kynos::http::StatusCode::FORBIDDEN)
.with_detail("d")
.with_extension("accounts", serde_json::json!(["/a"])),
balance: 30,
};
assert_eq!(
serde_json::to_value(&extended).expect("the value serializes"),
serde_json::json!({
"accounts": ["/a"], "balance": 30, "detail": "d",
"status": 403, "title": "Forbidden", "type": "about:blank"
})
);
let refusals = refusals(&extended);
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Extended>()
);
let schema = emitted::<Extended>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
assert!(
!validator.is_valid(&serde_json::json!({
"type": "about:blank", "status": "403", "balance": 30
})),
"a problem's `status` was accepted as a string: {schema}"
);
}
#[derive(Schema, Serialize)]
struct Throttled {
#[serde(flatten)]
problem: kynos::Problem,
#[serde(flatten)]
#[schema(open)]
limits: BTreeMap<String, u64>,
}
#[test]
fn a_flattened_problem_beside_an_open_map_leaves_the_maps_values_unchecked() {
let throttled = Throttled {
problem: kynos::Problem::new(kynos::http::StatusCode::TOO_MANY_REQUESTS)
.with_extension("retry", serde_json::json!("later")),
limits: BTreeMap::from([("remaining".to_owned(), 0)]),
};
let refusals = refusals(&throttled);
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Throttled>()
);
let schema = emitted::<Throttled>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
assert!(
validator.is_valid(&serde_json::json!({
"type": "about:blank", "status": 429, "remaining": "many"
})),
"a string under a map key was refused, so the recorded looseness no longer holds: {schema}"
);
}
#[derive(Schema, Serialize)]
struct Envelope {
id: u64,
#[serde(flatten)]
#[schema(open)]
rest: kynos::schema::unchecked::Unchecked<serde_json::Map<String, serde_json::Value>>,
}
fn envelope() -> Envelope {
Envelope {
id: 1,
rest: kynos::schema::unchecked::Unchecked(
serde_json::json!({ "k": "v", "n": [1, { "deep": null }] })
.as_object()
.expect("an object literal")
.clone(),
),
}
}
#[test]
fn an_open_unchecked_field_accepts_whatever_it_contributes() {
let refusals = refusals(&envelope());
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Envelope>()
);
let schema = emitted::<Envelope>();
assert!(
schema["unevaluatedProperties"].is_null(),
"an unconstrained payload closed the object it was flattened into: {schema}"
);
}
#[test]
fn an_open_unchecked_field_still_constrains_the_parents_own_properties() {
let schema = emitted::<Envelope>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
assert!(
!validator.is_valid(&serde_json::json!({ "id": "1", "k": "v" })),
"the parent's `id` was accepted as a string: {schema}"
);
assert!(
!validator.is_valid(&serde_json::json!({ "k": "v" })),
"the parent's own required property was not required: {schema}"
);
}
#[derive(Schema, Serialize)]
#[serde(tag = "outcome")]
enum Outcome {
Failed(kynos::Problem),
Done { id: u64 },
}
#[test]
fn a_problem_as_a_tagged_newtype_payload_accepts_what_serde_writes() {
let failed = Outcome::Failed(
kynos::Problem::new(kynos::http::StatusCode::CONFLICT)
.with_extension("retry", serde_json::json!(true)),
);
assert_eq!(
serde_json::to_value(&failed).expect("the value serializes"),
serde_json::json!({
"outcome": "Failed", "retry": true,
"status": 409, "title": "Conflict", "type": "about:blank"
})
);
let refusals = refusals(&failed);
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Outcome>()
);
let schema = emitted::<Outcome>();
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
assert!(
!validator.is_valid(&serde_json::json!({
"outcome": "Failed", "type": "about:blank", "status": "409"
})),
"a problem's `status` was accepted as a string: {schema}"
);
assert!(
!validator.is_valid(&serde_json::json!({
"outcome": "Lost", "type": "about:blank", "status": 409
})),
"a tag naming no variant was accepted: {schema}"
);
let done = Outcome::Done { id: 1 };
let refusals = self::refusals(&done);
assert!(
refusals.is_empty(),
"the sibling struct variant is refused beside the problem branch: {refusals:?}\n\
schema: {schema}"
);
}
#[derive(Schema, Serialize)]
struct Tallies {
id: u64,
#[serde(flatten)]
#[schema(open)]
counts: kynos::schema::unchecked::Unchecked<BTreeMap<String, u64>>,
}
#[test]
fn an_open_unchecked_typed_map_accepts_what_serde_writes_and_waives_its_values() {
let tallies = Tallies {
id: 1,
counts: kynos::schema::unchecked::Unchecked(BTreeMap::from([
("apples".to_owned(), 3),
("pears".to_owned(), 0),
])),
};
assert_eq!(
serde_json::to_value(&tallies).expect("the value serializes"),
serde_json::json!({ "id": 1, "apples": 3, "pears": 0 })
);
let refusals = refusals(&tallies);
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Tallies>()
);
let schema = emitted::<Tallies>();
assert!(
schema["unevaluatedProperties"].is_null(),
"an unchecked payload hoisted its map's value schema: {schema}"
);
let validator =
jsonschema::draft202012::new(&schema).expect("an emitted schema compiles as draft 2020-12");
assert!(
validator.is_valid(&serde_json::json!({ "id": 1, "apples": "many" })),
"a value the waived map schema would refuse was refused: {schema}"
);
}
#[derive(Schema, Serialize)]
struct Stamped {
id: u64,
#[serde(skip_deserializing)]
stamp: u64,
#[serde(flatten)]
#[schema(open)]
rest: kynos::schema::unchecked::Unchecked<serde_json::Map<String, serde_json::Value>>,
}
#[test]
fn a_field_serde_never_reads_beside_an_open_unchecked_field_is_admitted() {
let stamped = Stamped {
id: 1,
stamp: 7,
rest: kynos::schema::unchecked::Unchecked(
serde_json::json!({ "k": "v" })
.as_object()
.expect("an object literal")
.clone(),
),
};
assert_eq!(
serde_json::to_value(&stamped).expect("the value serializes"),
serde_json::json!({ "id": 1, "stamp": 7, "k": "v" })
);
let refusals = refusals(&stamped);
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Stamped>()
);
let schema = emitted::<Stamped>();
assert!(
schema["properties"].get("stamp").is_none(),
"a field serde never reads was described: {schema}"
);
}
#[derive(Schema, Serialize)]
struct Tagged {
id: u64,
#[serde(skip_deserializing)]
stamp: u64,
#[serde(flatten)]
#[schema(open)]
labels:
std::collections::HashMap<String, kynos::schema::unchecked::Unchecked<serde_json::Value>>,
}
#[test]
fn a_field_serde_never_reads_beside_an_open_map_of_unchecked_values_is_admitted() {
let tagged = Tagged {
id: 1,
stamp: 7,
labels: std::collections::HashMap::from([(
"k".to_owned(),
kynos::schema::unchecked::Unchecked(serde_json::json!({ "deep": [1, null] })),
)]),
};
assert_eq!(
serde_json::to_value(&tagged).expect("the value serializes"),
serde_json::json!({ "id": 1, "stamp": 7, "k": { "deep": [1, null] } })
);
let refusals = refusals(&tagged);
assert!(
refusals.is_empty(),
"the type cannot produce an instance its own description accepts: {refusals:?}\n\
schema: {}",
emitted::<Tagged>()
);
let schema = emitted::<Tagged>();
assert!(
schema["properties"].get("stamp").is_none(),
"a field serde never reads was described: {schema}"
);
assert!(
schema["unevaluatedProperties"]["x-kynos-unchecked"] == true,
"the hoisted value schema is not the permissive one: {schema}"
);
}