anyOf over string schemas generates #[serde(flatten)] on String, causing runtime error “can only flatten structs and maps (got a string)”
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 45/100
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.
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).
- Dominant language
- Rust
- Stars
- 898
- Forks
- 114
- Avg merge
- 4h 18m
- Merged PRs (30d)
- 14
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from oxidecomputer/typify
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
oxidecomputer/typify#1077 · 1 comment ·
-
Difficulty 3/5 1-2 days Newbie friendliness 55/100
oxidecomputer/typify#1075 · 1 comment ·
-
Difficulty 4/5 3-5 days Newbie friendliness 50/100
oxidecomputer/typify#1060 ·
-
Difficulty 5/5 Over a week Newbie friendliness 48/100
oxidecomputer/typify#1059 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 65/100
oxidecomputer/typify#1022 · 1 comment ·
All issues in oxidecomputer/typify
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
ontola/atomic-server#1625 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
VirusTotal/yara-x#777 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
stratum-mining/stratum#2404 ·
-
bug ci good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 88/100