@@ -2085,6 +2085,7 @@ function findDiscriminatorField(entries: { fieldName: string; typeName: string;
20852085 if ( ! fieldCandidates . has ( prop . name ) ) {
20862086 fieldCandidates . set ( prop . name , new Map ( ) ) ;
20872087 }
2088+
20882089 const mapping = fieldCandidates . get ( prop . name ) ! ;
20892090 if ( ! mapping . has ( prop . type . value ) ) {
20902091 mapping . set ( prop . type . value , entry ) ;
@@ -2120,6 +2121,10 @@ function findDiscriminatorField(entries: { fieldName: string; typeName: string;
21202121 return { fieldName : bestField , mapping : bestMapping , unmapped } ;
21212122}
21222123
2124+ function hasCustomStructureCodec ( name : string ) : boolean {
2125+ return name === "Registration" ;
2126+ }
2127+
21232128/**
21242129 * For a group of union entries that share the same JSON kind, find fields whose
21252130 * presence/absence in the JSON uniquely identifies a variant. A "presence discriminator"
@@ -2207,6 +2212,51 @@ function generateCode() {
22072212 return exhaustive ;
22082213 }
22092214
2215+ /**
2216+ * Generate streaming discriminator dispatch for unions with at most one
2217+ * unmapped fallback arm. Fields after the discriminator decode directly
2218+ * from the active decoder; fields before it are replayed by the helper.
2219+ */
2220+ function generateStreamingDiscriminatorDispatch (
2221+ name : string ,
2222+ disc : NonNullable < ReturnType < typeof findDiscriminatorField > > ,
2223+ indent : string ,
2224+ ) {
2225+ writeLine ( `${ indent } state, err := scanDiscriminatedStruct(dec, "${ name } ", ${ JSON . stringify ( disc . fieldName ) } )` ) ;
2226+ writeLine ( `${ indent } if err != nil {` ) ;
2227+ writeLine ( `${ indent } \treturn err` ) ;
2228+ writeLine ( `${ indent } }` ) ;
2229+ writeLine ( `${ indent } switch string(state.discriminatorValue) {` ) ;
2230+ for ( const [ value , entry ] of disc . mapping ) {
2231+ writeLine ( `${ indent } case \`"${ value } "\`:` ) ;
2232+ writeLine ( `${ indent } \treturn unmarshalDiscriminatedArm(state, &o.${ entry . fieldName } )` ) ;
2233+ }
2234+ writeLine ( `${ indent } default:` ) ;
2235+ if ( disc . unmapped . length === 1 ) {
2236+ writeLine ( `${ indent } \treturn unmarshalDiscriminatedArm(state, &o.${ disc . unmapped [ 0 ] . fieldName } )` ) ;
2237+ }
2238+ else {
2239+ writeLine ( `${ indent } \treturn state.invalidDiscriminator()` ) ;
2240+ }
2241+ writeLine ( `${ indent } }` ) ;
2242+ }
2243+
2244+ function canStreamDiscriminator (
2245+ disc : NonNullable < ReturnType < typeof findDiscriminatorField > > ,
2246+ ) : boolean {
2247+ if ( disc . unmapped . length > 1 ) {
2248+ return false ;
2249+ }
2250+ const entries = [ ...disc . mapping . values ( ) , ...disc . unmapped ] ;
2251+ return entries . every ( entry => {
2252+ if ( entry . originalType . kind !== "reference" ) {
2253+ return false ;
2254+ }
2255+ const name = entry . originalType . name ;
2256+ return ! hasCustomStructureCodec ( name ) && model . structures . some ( structure => structure . name === name ) ;
2257+ } ) ;
2258+ }
2259+
22102260 /**
22112261 * Generate try-each fallback code for unmapped entries, chaining into
22122262 * presence dispatch if possible before falling back to raw try-each.
@@ -2547,8 +2597,8 @@ function generateCode() {
25472597 writeLine ( "" ) ;
25482598 }
25492599
2550- // Generate UnmarshalJSONFrom method for structure validation
2551- // Skip Registration (has custom marshal/unmarshal generated separately)
2600+ // Generate UnmarshalJSONFrom method for structure validation.
2601+ // Structures with custom codecs are generated separately.
25522602 // Skip properties marked with omitzeroValue since they're optional by nature
25532603 const requiredProps = structure . properties ?. filter ( p => {
25542604 if ( p . optional ) return false ;
@@ -2562,7 +2612,7 @@ function generateCode() {
25622612 const resolved = resolveType ( p . type ) ;
25632613 return p . optional || resolved . needsPointer || resolved . name . startsWith ( "[]" ) || resolved . name . startsWith ( "map[" ) ;
25642614 } ) || false ;
2565- if ( ( requiredProps . length > 0 || hasNullRejectableFields ) && structure . name !== "Registration" ) {
2615+ if ( ( requiredProps . length > 0 || hasNullRejectableFields ) && ! hasCustomStructureCodec ( structure . name ) ) {
25662616 writeLine ( `\tvar _ json.UnmarshalerFrom = (*${ structure . name } )(nil)` ) ;
25672617 writeLine ( "" ) ;
25682618 writeLine ( `func (s *${ structure . name } ) UnmarshalJSONFrom(dec *json.Decoder) error {` ) ;
@@ -3392,17 +3442,24 @@ function generateCode() {
33923442 }
33933443 }
33943444 else {
3395- // Ambiguous: buffer and dispatch
3396- writeLine ( `\t\tdata, err := dec.ReadValue()` ) ;
3397- writeLine ( `\t\tif err != nil {` ) ;
3398- writeLine ( `\t\t\treturn err` ) ;
3399- writeLine ( `\t\t}` ) ;
34003445 let exhaustive = false ;
34013446 const disc = findDiscriminatorField ( entries ) ;
3402- if ( disc ) {
3403- exhaustive = generateDiscriminatorDispatch ( disc , "\t\t" ) ;
3447+ if ( disc && canStreamDiscriminator ( disc ) ) {
3448+ generateStreamingDiscriminatorDispatch ( name , disc , "\t\t" ) ;
3449+ exhaustive = true ;
34043450 }
34053451 else {
3452+ // Ambiguous non-discriminated objects need the complete
3453+ // value for presence checks or speculative decoding.
3454+ writeLine ( `\t\tdata, err := dec.ReadValue()` ) ;
3455+ writeLine ( `\t\tif err != nil {` ) ;
3456+ writeLine ( `\t\t\treturn err` ) ;
3457+ writeLine ( `\t\t}` ) ;
3458+ }
3459+ if ( disc && ! canStreamDiscriminator ( disc ) ) {
3460+ exhaustive = generateDiscriminatorDispatch ( disc , "\t\t" ) ;
3461+ }
3462+ else if ( ! disc ) {
34063463 const pres = findPresenceDiscriminator ( entries ) ;
34073464 if ( pres ) {
34083465 exhaustive = generatePresenceDispatch ( pres , "\t\t" ) ;
@@ -3428,25 +3485,30 @@ function generateCode() {
34283485 writeLine ( `\t}` ) ;
34293486 }
34303487 else {
3431- // Fallback: unknown kinds present (e.g. `any`), use ReadValue + try-each.
3432- writeLine ( "\tdata, err := dec.ReadValue()" ) ;
3433- writeLine ( "\tif err != nil {" ) ;
3434- writeLine ( "\t\treturn err" ) ;
3435- writeLine ( "\t}" ) ;
3436-
3437- if ( unionContainedNull ) {
3438- writeLine ( `\tif string(data) == "null" {` ) ;
3439- writeLine ( `\t\treturn nil` ) ;
3440- writeLine ( `\t}` ) ;
3441- writeLine ( "" ) ;
3442- }
3443-
3488+ // Fallback for unknown kinds (e.g. `any`). Discriminated object
3489+ // unions can still stream; other unions use ReadValue + try-each.
34443490 let exhaustive = false ;
34453491 const disc = findDiscriminatorField ( fieldEntries ) ;
3446- if ( disc ) {
3447- exhaustive = generateDiscriminatorDispatch ( disc , "\t" ) ;
3492+ if ( disc && canStreamDiscriminator ( disc ) ) {
3493+ generateStreamingDiscriminatorDispatch ( name , disc , "\t" ) ;
3494+ exhaustive = true ;
34483495 }
34493496 else {
3497+ writeLine ( "\tdata, err := dec.ReadValue()" ) ;
3498+ writeLine ( "\tif err != nil {" ) ;
3499+ writeLine ( "\t\treturn err" ) ;
3500+ writeLine ( "\t}" ) ;
3501+ if ( unionContainedNull ) {
3502+ writeLine ( `\tif string(data) == "null" {` ) ;
3503+ writeLine ( `\t\treturn nil` ) ;
3504+ writeLine ( `\t}` ) ;
3505+ writeLine ( "" ) ;
3506+ }
3507+ }
3508+ if ( disc && ! canStreamDiscriminator ( disc ) ) {
3509+ exhaustive = generateDiscriminatorDispatch ( disc , "\t" ) ;
3510+ }
3511+ else if ( ! disc ) {
34503512 const pres = findPresenceDiscriminator ( fieldEntries ) ;
34513513 if ( pres ) {
34523514 exhaustive = generatePresenceDispatch ( pres , "\t" ) ;
0 commit comments