diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index 5c902a4c..567b6609 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -1461,6 +1461,15 @@ impl TypeSpace { metadata: &'a Option>, subschemas: &'a [Schema], ) -> Result<(TypeEntry, &'a Option>)> { + // An empty `anyOf` cannot match any instance. + if subschemas.is_empty() { + let type_name = match get_type_name(&type_name, metadata) { + Some(name) => Name::Required(name), + None => type_name, + }; + return self.convert_never(type_name, original_schema); + } + // Rust can emit "anyOf":[{"$ref":"#/definitions/C"},{"type":"null"} // for Option. We match this here because the mutual exclusion check // below may fail for cases such as Option where T is defined to be, @@ -2294,6 +2303,26 @@ mod tests { } } + #[test] + fn test_empty_any_of() { + let schema_json = r#" + { + "title": "EmptyAnyOf", + "anyOf": [] + } + "#; + + let schema: RootSchema = serde_json::from_str(schema_json).unwrap(); + + let mut type_space = TypeSpace::default(); + let type_id = type_space.add_type(&schema.schema.into()).unwrap(); + + match &type_space.id_to_entry[&type_id].details { + super::TypeEntryDetails::Enum(details) => assert!(details.variants.is_empty()), + details => panic!("empty anyOf should be uninhabited, got {details:?}"), + } + } + #[test] fn test_overridden_conversion() { let schema_json = r#" diff --git a/typify-impl/src/util.rs b/typify-impl/src/util.rs index 54d232b9..982a2469 100644 --- a/typify-impl/src/util.rs +++ b/typify-impl/src/util.rs @@ -48,6 +48,12 @@ pub(crate) fn all_mutually_exclusive( definitions: &BTreeMap, ) -> bool { let len = subschemas.len(); + // With fewer than two subschemas there are no pairs to compare, so the + // schemas are vacuously mutually exclusive. This also avoids underflow + // in `len - 1` below when `subschemas` is empty. + if len < 2 { + return true; + } // Consider all pairs (0..len - 1) .flat_map(|ii| (ii + 1..len).map(move |jj| (ii, jj))) @@ -1001,7 +1007,10 @@ mod tests { }; use crate::{ - util::{decode_segment, sanitize, schemas_mutually_exclusive, Case, ReorderedInstanceType}, + util::{ + all_mutually_exclusive, decode_segment, sanitize, schemas_mutually_exclusive, Case, + ReorderedInstanceType, + }, Name, }; @@ -1107,6 +1116,14 @@ mod tests { assert!(schemas_mutually_exclusive(&b, &a, &BTreeMap::new())); } + #[test] + fn test_all_mutually_exclusive_empty() { + // An empty slice of subschemas has no pairs to compare, so it's + // vacuously true. This should not panic (previously `len - 1` + // underflowed for an empty slice). + assert!(all_mutually_exclusive(&[], &BTreeMap::new())); + } + #[test] fn test_decode_segment() { assert_eq!(decode_segment("foo~1bar"), "foo/bar");