-
Notifications
You must be signed in to change notification settings - Fork 98
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
-
Generate with Progenitor against the schema snippet above.
-
Serialize an instance:
let v = types::OrderCreationAppData { subtype_0: Some("{}".to_string().into()), subtype_1: None, }; serde_json::to_string(&v).unwrap(); // panics
-
Observe:
Error("can only flatten structs and maps (got a string)").
Environment
- Progenitor version: latest
Workarounds
- Build request body as
serde_json::Valueand 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
OrderCreationAppDatatype until this is fixed.
Related
- StackOverflow report showing the same crash path with Progenitor &
anyOfincluding a string. ([Stack Overflow][1]) - Serde docs on
flattenlimitations. ([serde.rs][2])
Proposed fix
In Typify/Progenitor’s codegen for oneOf/anyOf:
- Detect when all variants resolve to non-object schemas (e.g., primitives, strings, numbers).
- Do not emit a struct-with-
flatten. - Prefer
#[serde(untagged)] enumfor these unions; or, if variants are the same primitive type, consider a simple newtype around that primitive (with optional validation).