From 7f207accfbbd200743c78b2ce467809adb77ee33 Mon Sep 17 00:00:00 2001 From: Vadim Volodin Date: Tue, 24 Feb 2026 21:37:51 +0800 Subject: [PATCH] feat: flatten allOf when there are multiple oneOfs inside --- typify-impl/src/convert.rs | 57 ++- .../schemas/flatten_allOf_of_oneOfs.json | 71 ++++ .../tests/schemas/flatten_allOf_of_oneOfs.rs | 368 ++++++++++++++++++ 3 files changed, 494 insertions(+), 2 deletions(-) create mode 100644 typify/tests/schemas/flatten_allOf_of_oneOfs.json create mode 100644 typify/tests/schemas/flatten_allOf_of_oneOfs.rs diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index 9f4da2e8..a2f94027 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -1,6 +1,6 @@ // Copyright 2025 Oxide Computer Company -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use crate::merge::{merge_all, try_merge_with_subschemas}; use crate::type_entry::{ @@ -16,7 +16,7 @@ use schemars::schema::{ use crate::util::get_type_name; -use crate::{Error, Name, Result, TypeSpace, TypeSpaceImpl}; +use crate::{Error, Name, RefKey, Result, TypeSpace, TypeSpaceImpl}; pub const STD_NUM_NONZERO_PREFIX: &str = "::std::num::NonZero"; @@ -1427,6 +1427,25 @@ impl TypeSpace { // optional field; a number whose value is limited can be converted to // the more expansive numeric type. + // If two or more allOf elements contain oneOf/anyOf subschemas, + // merging them produces degenerate results (nested allOf/not that + // become empty enums). Instead, use a flattened struct where each + // subschema becomes a flattened field. This matches the original + // Rust pattern of two flattened internally-tagged enums. + let subschema_count = subschemas + .iter() + .filter(|s| schema_has_one_of_or_any_of(s, &self.definitions)) + .count(); + if subschema_count >= 2 { + return self.flattened_union_struct( + type_name, + original_schema, + metadata, + subschemas, + false, + ); + } + let merged_schema = merge_all(subschemas, &self.definitions); if let Schema::Bool(false) = &merged_schema { self.convert_never(type_name, original_schema) @@ -2075,6 +2094,40 @@ impl TypeSpace { } } +/// Check if a schema (possibly a $ref) contains a oneOf or anyOf subschema. +fn schema_has_one_of_or_any_of( + schema: &Schema, + definitions: &BTreeMap, +) -> bool { + let resolved = match schema { + Schema::Object(SchemaObject { + metadata: _, + instance_type: None, + format: None, + enum_values: None, + const_value: None, + subschemas: None, + number: None, + string: None, + array: None, + object: None, + reference: Some(ref_name), + extensions: _, + }) => definitions + .get(&ref_key(ref_name)) + .unwrap_or(schema), + _ => schema, + }; + + matches!( + resolved, + Schema::Object(SchemaObject { + subschemas: Some(sub), + .. + }) if sub.one_of.is_some() || sub.any_of.is_some() + ) +} + #[cfg(test)] mod tests { use std::num::{NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8}; diff --git a/typify/tests/schemas/flatten_allOf_of_oneOfs.json b/typify/tests/schemas/flatten_allOf_of_oneOfs.json new file mode 100644 index 00000000..5ba49c17 --- /dev/null +++ b/typify/tests/schemas/flatten_allOf_of_oneOfs.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://spec.openapis.org/oas/3.0/schema/2024-10-18#/definitions/Schema", + "title": "Item", + "type": "object", + "allOf": [ + { + "oneOf": [ + { + "type": "object", + "properties": { + "myfirsttag": { + "type": "string", + "enum": [ + "a" + ] + } + }, + "required": [ + "myfirsttag" + ] + }, + { + "type": "object", + "properties": { + "myfirsttag": { + "type": "string", + "enum": [ + "b" + ] + } + }, + "required": [ + "myfirsttag" + ] + } + ] + }, + { + "oneOf": [ + { + "type": "object", + "properties": { + "mysecondtag": { + "type": "string", + "enum": [ + "c" + ] + } + }, + "required": [ + "mysecondtag" + ] + }, + { + "type": "object", + "properties": { + "mysecondtag": { + "type": "string", + "enum": [ + "d" + ] + } + }, + "required": [ + "mysecondtag" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/typify/tests/schemas/flatten_allOf_of_oneOfs.rs b/typify/tests/schemas/flatten_allOf_of_oneOfs.rs new file mode 100644 index 00000000..c6f9df71 --- /dev/null +++ b/typify/tests/schemas/flatten_allOf_of_oneOfs.rs @@ -0,0 +1,368 @@ +#![deny(warnings)] +#[doc = r" Error types."] +pub mod error { + #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + impl ::std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } +} +#[doc = "`Item`"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"title\": \"Item\","] +#[doc = " \"type\": \"object\","] +#[doc = " \"allOf\": ["] +#[doc = " {"] +#[doc = " \"oneOf\": ["] +#[doc = " {"] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"myfirsttag\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"myfirsttag\": {"] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"a\""] +#[doc = " ]"] +#[doc = " }"] +#[doc = " }"] +#[doc = " },"] +#[doc = " {"] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"myfirsttag\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"myfirsttag\": {"] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"b\""] +#[doc = " ]"] +#[doc = " }"] +#[doc = " }"] +#[doc = " }"] +#[doc = " ]"] +#[doc = " },"] +#[doc = " {"] +#[doc = " \"oneOf\": ["] +#[doc = " {"] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"mysecondtag\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"mysecondtag\": {"] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"c\""] +#[doc = " ]"] +#[doc = " }"] +#[doc = " }"] +#[doc = " },"] +#[doc = " {"] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"mysecondtag\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"mysecondtag\": {"] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"d\""] +#[doc = " ]"] +#[doc = " }"] +#[doc = " }"] +#[doc = " }"] +#[doc = " ]"] +#[doc = " }"] +#[doc = " ]"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +pub struct Item { + #[serde(flatten)] + pub subtype_0: ItemSubtype0, + #[serde(flatten)] + pub subtype_1: ItemSubtype1, +} +impl Item { + pub fn builder() -> builder::Item { + Default::default() + } +} +#[doc = "`ItemSubtype0`"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"oneOf\": ["] +#[doc = " {"] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"myfirsttag\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"myfirsttag\": {"] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"a\""] +#[doc = " ]"] +#[doc = " }"] +#[doc = " }"] +#[doc = " },"] +#[doc = " {"] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"myfirsttag\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"myfirsttag\": {"] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"b\""] +#[doc = " ]"] +#[doc = " }"] +#[doc = " }"] +#[doc = " }"] +#[doc = " ]"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +#[serde(tag = "myfirsttag")] +pub enum ItemSubtype0 { + #[serde(rename = "a")] + A, + #[serde(rename = "b")] + B, +} +impl ::std::fmt::Display for ItemSubtype0 { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::A => f.write_str("a"), + Self::B => f.write_str("b"), + } + } +} +impl ::std::str::FromStr for ItemSubtype0 { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "a" => Ok(Self::A), + "b" => Ok(Self::B), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for ItemSubtype0 { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for ItemSubtype0 { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for ItemSubtype0 { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +#[doc = "`ItemSubtype1`"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"oneOf\": ["] +#[doc = " {"] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"mysecondtag\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"mysecondtag\": {"] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"c\""] +#[doc = " ]"] +#[doc = " }"] +#[doc = " }"] +#[doc = " },"] +#[doc = " {"] +#[doc = " \"type\": \"object\","] +#[doc = " \"required\": ["] +#[doc = " \"mysecondtag\""] +#[doc = " ],"] +#[doc = " \"properties\": {"] +#[doc = " \"mysecondtag\": {"] +#[doc = " \"type\": \"string\","] +#[doc = " \"enum\": ["] +#[doc = " \"d\""] +#[doc = " ]"] +#[doc = " }"] +#[doc = " }"] +#[doc = " }"] +#[doc = " ]"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, +)] +#[serde(tag = "mysecondtag")] +pub enum ItemSubtype1 { + #[serde(rename = "c")] + C, + #[serde(rename = "d")] + D, +} +impl ::std::fmt::Display for ItemSubtype1 { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::C => f.write_str("c"), + Self::D => f.write_str("d"), + } + } +} +impl ::std::str::FromStr for ItemSubtype1 { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "c" => Ok(Self::C), + "d" => Ok(Self::D), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for ItemSubtype1 { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for ItemSubtype1 { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for ItemSubtype1 { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +#[doc = r" Types for composing complex structures."] +pub mod builder { + #[derive(Clone, Debug)] + pub struct Item { + subtype_0: ::std::result::Result, + subtype_1: ::std::result::Result, + } + impl ::std::default::Default for Item { + fn default() -> Self { + Self { + subtype_0: Err("no value supplied for subtype_0".to_string()), + subtype_1: Err("no value supplied for subtype_1".to_string()), + } + } + } + impl Item { + pub fn subtype_0(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.subtype_0 = value + .try_into() + .map_err(|e| format!("error converting supplied value for subtype_0: {e}")); + self + } + pub fn subtype_1(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.subtype_1 = value + .try_into() + .map_err(|e| format!("error converting supplied value for subtype_1: {e}")); + self + } + } + impl ::std::convert::TryFrom for super::Item { + type Error = super::error::ConversionError; + fn try_from(value: Item) -> ::std::result::Result { + Ok(Self { + subtype_0: value.subtype_0?, + subtype_1: value.subtype_1?, + }) + } + } + impl ::std::convert::From for Item { + fn from(value: super::Item) -> Self { + Self { + subtype_0: Ok(value.subtype_0), + subtype_1: Ok(value.subtype_1), + } + } + } +} +fn main() {}