Repository navigation
Description
Summary
An object schema with type: object and properties that also has an allOf of
validation-only if/then rules generates as serde_json::Value. A discriminated oneOf
that has this schema as a branch turns from tagged to #[serde(untagged)].
The rules do not change the shape of the object. They only constrain values, for example
"if zone is utc, name must end with Z".
Version: openapi-to-rust 0.19.0. Command:
openapi-to-rust generate spec.json --types-only --module-name generated --output-dir out.
Reproduction
{
"openapi": "3.1.0",
"info": {"title": "repro", "version": "1"},
"paths": {},
"components": {"schemas": {
"Pet": {
"oneOf": [{"$ref": "#/components/schemas/Cat"}, {"$ref": "#/components/schemas/Dog"}],
"discriminator": {"propertyName": "kind"}
},
"Cat": {
"type": "object",
"required": ["kind", "name"],
"properties": {"kind": {"type": "string", "const": "Cat"}, "name": {"type": "string"}, "zone": {"type": "string"}},
"allOf": [{"if": {"properties": {"zone": {"const": "utc"}}, "required": ["zone"]}, "then": {"properties": {"name": {"pattern": "Z$"}}}}]
},
"Dog": {
"type": "object",
"required": ["kind", "name"],
"properties": {"kind": {"type": "string", "const": "Dog"}, "name": {"type": "string"}}
}
}}
}Actual output (excerpt):
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum Pet {
Cat(Cat),
Dog(Dog),
}
pub type Cat = serde_json::Value;Expected output: the same as without the allOf. Cat is a struct with kind, name
and zone, and Pet keeps the tagged serialization on kind. If you remove the allOf
from Cat, the generator emits exactly that.
Cause
Schemais an untagged serde enum (src/openapi.rs). TheAllOfcase comes before
the object case, soCatdeserializes asSchema::AllOf. Itstypeandproperties
go intodetails.branch_resolves_to_object_inner(src/analysis.rs, about line 2365) decides from the
allOfmembers only, and returns before it reads the schema's own type: "Other allOf
shapes are object-like only when at least one meaningful member is." Anif/then
member has no type, so the result isfalse.- The
oneOfanalysis (src/analysis.rs, about lines 4797-4813) falls back to an
untagged union when any branch does not resolve to an object.
Suggested fix
- In
branch_resolves_to_object_inner, treat aSchema::AllOfas object-like when its own
schema_typeisobjector itsdetailshaveproperties. - Treat an
allOfmember that holds onlyif,thenandelse(and annotations) as
validation-only. It should not change the generated type.
Related: #65 (a base object combined with a variant union), a different shape with the same
serde_json::Value fallback.
Context
SiennaSchemas adds such rules
to the five branches of its TimeSeriesAssociation union. Until a fix is released, our
bundler removes rule-only allOf members before generation.
Activity
Follow RFC 3339 for date-time and date-time-local fields
lightsofapollo commentedon Oct 29, 2026
on it
chore: refresh corpus manifest for #111/#112 output changes
lightsofapollo commentedon Oct 29, 2026
Fixed in #113. Root cause was broader than
branch_resolves_to_object:analyze_allof_compositioncollapsed a single-memberallOfto that member without looking at the owner's owntype: object/properties, so the loneif/thenmember (untyped) replacedCat. Now conditional-only (if/then/else+ annotations) members don't shape the type, owners with their own properties always stay objects, and union branch detection agrees. Your repro now generates byte-identical output to the same spec without theallOf. The same fix restores owner fields dropped by{properties, allOf: [$ref]}(e.g. in Cloudflare/gcore). Will ship in the next release.jd-lara commentedon Oct 30, 2026
Thanks a lot for the quick fix
Pin openapi-to-rust to the fix for rule-only allOf