@@ -9,6 +9,8 @@ use std::path::PathBuf;
99struct DiscriminatedVariantInfo {
1010 /// The discriminator field name (e.g., "type")
1111 discriminator_field : String ,
12+ /// The const value of the discriminator (e.g., "text")
13+ discriminator_value : String ,
1214 /// Whether the parent union is untagged
1315 is_parent_untagged : bool ,
1416}
@@ -206,6 +208,7 @@ impl CodeGenerator {
206208 variant. type_name . clone ( ) ,
207209 DiscriminatedVariantInfo {
208210 discriminator_field : discriminator_field. clone ( ) ,
211+ discriminator_value : variant. discriminator_value . clone ( ) ,
209212 is_parent_untagged,
210213 } ,
211214 ) ;
@@ -603,8 +606,52 @@ impl CodeGenerator {
603606 }
604607 }
605608 SchemaType :: Array { item_type } => {
606- // Generate type alias for named array schemas
609+ // Generate type alias for named array schemas.
610+ //
611+ // Special case: if the array item is a struct whose discriminator
612+ // field was stripped (because it's used in a tagged enum), the bare
613+ // struct won't serialize the discriminator in standalone contexts.
614+ // Generate a single-variant tagged wrapper enum so the discriminator
615+ // field is re-added by serde's tag attribute.
607616 let array_name = format_ident ! ( "{}" , self . to_rust_type_name( & schema. name) ) ;
617+
618+ // Check if the item type is a Reference to a discriminator-stripped struct
619+ if let SchemaType :: Reference { target } = item_type. as_ref ( ) {
620+ if let Some ( info) = discriminated_variant_info. get ( target) {
621+ if !info. is_parent_untagged {
622+ // Generate a wrapper enum that re-adds the discriminator tag
623+ let wrapper_name = format_ident ! (
624+ "{}Item" ,
625+ self . to_rust_type_name( & schema. name)
626+ ) ;
627+ let variant_type =
628+ format_ident ! ( "{}" , self . to_rust_type_name( target) ) ;
629+ let disc_field = & info. discriminator_field ;
630+ let disc_value = & info. discriminator_value ;
631+
632+ let doc_comment = if let Some ( desc) = & schema. description {
633+ quote ! { #[ doc = #desc] }
634+ } else {
635+ TokenStream :: new ( )
636+ } ;
637+
638+ return Ok ( quote ! {
639+ /// Wrapper enum that re-adds the discriminator tag
640+ /// for array contexts where the inner struct had its
641+ /// discriminator field stripped for tagged enum use.
642+ #[ derive( Debug , Clone , Deserialize , Serialize ) ]
643+ #[ serde( tag = #disc_field) ]
644+ pub enum #wrapper_name {
645+ #[ serde( rename = #disc_value) ]
646+ #variant_type( #variant_type) ,
647+ }
648+ #doc_comment
649+ pub type #array_name = Vec <#wrapper_name>;
650+ } ) ;
651+ }
652+ }
653+ }
654+
608655 let inner_type = self . generate_array_item_type ( item_type, analysis) ;
609656
610657 let doc_comment = if let Some ( desc) = & schema. description {
@@ -856,7 +903,7 @@ impl CodeGenerator {
856903 let field_type =
857904 self . generate_field_type ( & schema. name , field_name, prop, is_required, analysis) ;
858905
859- let serde_attrs = self . generate_serde_field_attrs ( field_name, prop, is_required) ;
906+ let serde_attrs = self . generate_serde_field_attrs ( field_name, prop, is_required, analysis ) ;
860907 let specta_attrs = self . generate_specta_field_attrs ( field_name) ;
861908
862909 let doc_comment = if let Some ( desc) = & prop. description {
@@ -1235,7 +1282,13 @@ impl CodeGenerator {
12351282 . unwrap_or ( false ) ;
12361283
12371284 if is_required && !prop. nullable && !is_nullable_override {
1238- base_type
1285+ // If the field has a default value but its type doesn't implement Default,
1286+ // wrap in Option<T> so serde can default to None instead of requiring Default.
1287+ if prop. default . is_some ( ) && self . type_lacks_default ( & prop. schema_type , analysis) {
1288+ quote ! { Option <#base_type> }
1289+ } else {
1290+ base_type
1291+ }
12391292 } else {
12401293 quote ! { Option <#base_type> }
12411294 }
@@ -1246,6 +1299,7 @@ impl CodeGenerator {
12461299 field_name : & str ,
12471300 prop : & crate :: analysis:: PropertyInfo ,
12481301 is_required : bool ,
1302+ analysis : & crate :: analysis:: SchemaAnalysis ,
12491303 ) -> TokenStream {
12501304 let mut attrs = Vec :: new ( ) ;
12511305
@@ -1264,10 +1318,13 @@ impl CodeGenerator {
12641318 attrs. push ( quote ! { skip_serializing_if = "Option::is_none" } ) ;
12651319 }
12661320
1267- // Only add default attribute for required fields that have default values
1268- // Optional fields (Option<T>) already default to None, so don't need #[serde(default)]
1321+ // Only add default attribute for required fields that have default values.
1322+ // Skip #[serde(default)] for types that don't implement Default (discriminated
1323+ // unions, union enums) — those fields should be Option<T> instead.
12691324 if prop. default . is_some ( ) && ( is_required && !prop. nullable ) {
1270- attrs. push ( quote ! { default } ) ;
1325+ if !self . type_lacks_default ( & prop. schema_type , analysis) {
1326+ attrs. push ( quote ! { default } ) ;
1327+ }
12711328 }
12721329
12731330 if attrs. is_empty ( ) {
@@ -1277,6 +1334,28 @@ impl CodeGenerator {
12771334 }
12781335 }
12791336
1337+ /// Check if a schema type resolves to a type that doesn't implement `Default`.
1338+ /// Discriminated unions and union enums don't derive Default, so fields with
1339+ /// these types can't use `#[serde(default)]`.
1340+ fn type_lacks_default (
1341+ & self ,
1342+ schema_type : & crate :: analysis:: SchemaType ,
1343+ analysis : & crate :: analysis:: SchemaAnalysis ,
1344+ ) -> bool {
1345+ use crate :: analysis:: SchemaType ;
1346+ match schema_type {
1347+ SchemaType :: DiscriminatedUnion { .. } | SchemaType :: Union { .. } => true ,
1348+ SchemaType :: Reference { target } => {
1349+ if let Some ( schema) = analysis. schemas . get ( target) {
1350+ self . type_lacks_default ( & schema. schema_type , analysis)
1351+ } else {
1352+ false
1353+ }
1354+ }
1355+ _ => false ,
1356+ }
1357+ }
1358+
12801359 fn generate_specta_field_attrs ( & self , field_name : & str ) -> TokenStream {
12811360 if !self . config . enable_specta {
12821361 return TokenStream :: new ( ) ;
0 commit comments