oxidecomputer / oxidecomputer/typify

anyOf over string schemas generates #[serde(flatten)] on String, causing runtime error “can only flatten structs and maps (got a string)”

Open
#895 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

anyOf typify2
Dominant language
Rust
Stars
898
Forks
114
Avg merge
4h 18m
Merged PRs (30d)
14

Description

What happened
When an OpenAPI schema uses anyOf to allow either a string (with an allOf/$ref that resolves to type: string) or another string $ref, Progenitor generates a Rust struct with two optional fields, both annotated with #[serde(flatten)]. At runtime, serializing or deserializing this type panics with:

Error("can only flatten structs and maps (got a string)", line: 0, column: 0)

Minimal repro OpenAPI fragment

components:
  schemas:
    AppData:
      type: string
      description: String encoding of a JSON object (UTF-8).
    AppDataHash:
      type: string
      pattern: '^0x[0-9a-fA-F]{64}$'

    OrderCreation:
      type: object
      required: [appData]
      properties:
        appData:
          description: This field comes in two forms for backward compatibility.
          anyOf:
            - title: Full App Data
              description: String encoding of a JSON object.
              type: string
              allOf: [{ $ref: '#/components/schemas/AppData' }]
            - $ref: '#/components/schemas/AppDataHash'

You can use CoW's too. See OrderCreation AppData

Generated code (problematic)

#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
pub struct OrderCreationAppData {
    #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
    pub subtype_0: Option<AppData>,      // AppData = String
    #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
    pub subtype_1: Option<AppDataHash>,  // AppDataHash = String
}

Why this is wrong
Serde’s flatten only works when the field is a map/struct; it cannot flatten primitives like String. This is documented behavior. Attempting to serialize/deserialize yields the panic above. ([serde.rs][2])

Expected code
For anyOf/oneOf where all variants resolve to non-object types (e.g., strings), the generator should emit an untagged enum (or even a single String newtype if indistinguishable at runtime). For example:

#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
#[serde(untagged)]
pub enum OrderCreationAppData {
    Full(String),  // stringified JSON object
    Hash(String),  // 0x… hash
}

Alternatively, if an enum is undesirable here, generate one String (no flatten) and let the server-side validation discriminate — but under no circumstance emit flatten on a String.

How to reproduce

  1. Generate with Progenitor against the schema snippet above.

  2. Serialize an instance:

    let v = types::OrderCreationAppData {
        subtype_0: Some("{}".to_string().into()),
        subtype_1: None,
    };
    serde_json::to_string(&v).unwrap(); // panics
    
  3. Observe: Error("can only flatten structs and maps (got a string)").

Environment

  • Progenitor version: latest

Workarounds

  • Build request body as serde_json::Value and insert "appData" as a plain string.
  • Define a local DTO that mirrors the API (appData: String) and serialize that for requests.
  • Avoid serializing the generated OrderCreationAppData type until this is fixed.

Related

  • StackOverflow report showing the same crash path with Progenitor & anyOf including a string. ([Stack Overflow][1])
  • Serde docs on flatten limitations. ([serde.rs][2])

Proposed fix
In Typify/Progenitor’s codegen for oneOf/anyOf:

  1. Detect when all variants resolve to non-object schemas (e.g., primitives, strings, numbers).
  2. Do not emit a struct-with-flatten.
  3. Prefer #[serde(untagged)] enum for these unions; or, if variants are the same primitive type, consider a simple newtype around that primitive (with optional validation).

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with Typify/Progenitor code generation for oneOf and anyOf, then reproduce the issue with the provided OpenAPI schema and inspect the generated OrderCreationAppData type. Verify the generated type handles non-object string variants without serde(flatten), and confirm serialization and deserialization no longer produce the reported runtime error.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.