Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions typify-impl/src/convert.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -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";

Expand Down Expand Up @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this logic seem flawed. In particular it only seems sound if the fields of both subschemas are non-overlapping.

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)
Expand Down Expand Up @@ -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<RefKey, Schema>,
) -> 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};
Expand Down
71 changes: 71 additions & 0 deletions typify/tests/schemas/flatten_allOf_of_oneOfs.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
}
]
}
Loading