Skip to content

Commit f5dd9e7

Browse files
fix: do not invent a variant for structs that flatten one
A struct holding a flattened variant cannot derive `Default` — the untagged enum has no default, and choosing one would invent data the same way a required field would. The generated request builder has the same problem: it constructs the struct field by field and has no variant to supply. Both are now skipped when a variant is present, which is what Cloudflare, GitHub, LaunchDarkly, and Lithic needed to compile again. Refs #65 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TD3TSeWKu4VqLtEnRMDjry
1 parent d570d98 commit f5dd9e7

2 files changed

Lines changed: 40 additions & 3 deletions

File tree

src/generator.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2060,9 +2060,12 @@ impl CodeGenerator {
20602060
// additional-properties map (when present) is empty by default. We do
20612061
// not invent values for required data, even when the Rust type itself
20622062
// happens to implement Default.
2063-
let can_derive_default = emitted_properties
2064-
.iter()
2065-
.all(|property| !property.is_required);
2063+
// A flattened variant is one of several shapes, and picking one would
2064+
// invent data the same way a required field would.
2065+
let can_derive_default = variant.is_none()
2066+
&& emitted_properties
2067+
.iter()
2068+
.all(|property| !property.is_required);
20662069

20672070
// Generate derives with optional Specta support
20682071
// Note: We use snake_case everywhere (matching the OpenAPI spec) for consistency
@@ -2085,6 +2088,7 @@ impl CodeGenerator {
20852088
};
20862089

20872090
let builder = if type_context.index.request_body_roots.contains(&schema.name)
2091+
&& variant.is_none()
20882092
&& emitted_properties
20892093
.iter()
20902094
.any(|property| property.is_required)

tests/recoverable_typing_test.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,39 @@ fn an_object_that_also_declares_variants_keeps_both_halves() {
592592
);
593593
}
594594

595+
#[test]
596+
fn a_struct_with_a_flattened_variant_derives_no_default() {
597+
// `Default` on the struct would have to invent a variant, which is the same
598+
// problem as inventing a required field. Deriving it anyway does not
599+
// compile, since the untagged enum has no default either.
600+
let generated = generate(spec_with_schemas(json!({
601+
"A": { "type": "object", "additionalProperties": false,
602+
"properties": { "a": { "type": "string" } } },
603+
"B": { "type": "object", "additionalProperties": false,
604+
"properties": { "b": { "type": "string" } } },
605+
"Holder": {
606+
"properties": { "note": { "type": "string" } },
607+
"anyOf": [
608+
{ "$ref": "#/components/schemas/A" },
609+
{ "$ref": "#/components/schemas/B" }
610+
]
611+
}
612+
})));
613+
614+
let struct_start = generated
615+
.find("pub struct Holder")
616+
.expect("the struct is generated");
617+
let derive_line = generated[..struct_start]
618+
.lines()
619+
.rev()
620+
.find(|line| line.contains("#[derive("))
621+
.expect("a derive line precedes the struct");
622+
assert!(
623+
!derive_line.contains("Default"),
624+
"a struct holding a flattened variant must not derive Default: {derive_line}"
625+
);
626+
}
627+
595628
#[test]
596629
fn a_requiredness_only_union_inside_a_property_is_the_object_it_describes() {
597630
// The same shape one level down, which the named-schema check did not see:

0 commit comments

Comments
 (0)