Skip to content

allOf of if/then rules on an object schema turns it into serde_json::Value and untags its discriminated union #111

Closed
Sienna-Platform/power-openapi-models
#30

Description

@jd-lara

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

  1. Schema is an untagged serde enum (src/openapi.rs). The AllOf case comes before
    the object case, so Cat deserializes as Schema::AllOf. Its type and properties
    go into details.
  2. branch_resolves_to_object_inner (src/analysis.rs, about line 2365) decides from the
    allOf members 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." An if/then
    member has no type, so the result is false.
  3. The oneOf analysis (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 a Schema::AllOf as object-like when its own
    schema_type is object or its details have properties.
  • Treat an allOf member that holds only if, then and else (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

  1. lightsofapollo commented on Oct 29, 2026

    @lightsofapollo
    Contributor

    on it

  2. added a commit that references this issue on Oct 29, 2026
  3. lightsofapollo commented on Oct 29, 2026

    @lightsofapollo
    Contributor

    Fixed in #113. Root cause was broader than branch_resolves_to_object: analyze_allof_composition collapsed a single-member allOf to that member without looking at the owner's own type: object/properties, so the lone if/then member (untyped) replaced Cat. 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 the allOf. The same fix restores owner fields dropped by {properties, allOf: [$ref]} (e.g. in Cloudflare/gcore). Will ship in the next release.

  4. jd-lara commented on Oct 30, 2026

    @jd-lara
    Author

    Thanks a lot for the quick fix

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions