Skip to content
This repository was archived by the owner on Apr 14, 2026. It is now read-only.

Commit b5a25d0

Browse files
mromaszewiczclaude
andcommitted
feat: replace oneOf/anyOf with json.RawMessage union + typed accessors
Replace the try-all-variants pointer-per-field union pattern with the original oapi-codegen's proven json.RawMessage approach. Union types now have a private 'union json.RawMessage' field with typed As/From/Merge accessor methods for each variant. Key changes: - Union structs use json.RawMessage instead of pointer-per-variant - Generate As<Variant>(), From<Variant>(), Merge<Variant>() for each member - Add discriminator support: Discriminator() extracts the value from union data, ValueByDiscriminator() routes to the correct As method - Discriminator mapping supports both explicit (spec mapping) and implicit (inferred from $ref schema names) - Partial mappings handled gracefully (warn, don't error) - Add JSONMerge helper (template + runtime) for Merge methods - Use 'any' not 'interface{}' for ValueByDiscriminator return type Generated code pattern: type Pet struct { union json.RawMessage } func (t Pet) AsCat() (Cat, error) func (t *Pet) FromCat(v Cat) error func (t *Pet) MergeCat(v Cat) error func (t Pet) Discriminator() (string, error) // when present func (t Pet) ValueByDiscriminator() (any, error) // when mapped Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d066c9e commit b5a25d0

18 files changed

Lines changed: 3172 additions & 2001 deletions

File tree

experimental/codegen/internal/codegen.go

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,9 @@ func generateHelper(name string, ctx *CodegenContext) (string, error) {
413413
case "marshal_form":
414414
ctx.AddTemplateImports(templates.MarshalFormHelperTemplate.Imports)
415415
return generateMarshalFormHelper()
416+
case "json_merge":
417+
ctx.AddTemplateImports(templates.JSONMergeHelperTemplate.Imports)
418+
return generateJSONMergeHelper()
416419
default:
417420
return "", fmt.Errorf("unknown helper: %s", name)
418421
}
@@ -439,6 +442,27 @@ func generateMarshalFormHelper() (string, error) {
439442
return result.String(), nil
440443
}
441444

445+
// generateJSONMergeHelper generates the JSONMerge helper function.
446+
func generateJSONMergeHelper() (string, error) {
447+
tmplInfo := templates.JSONMergeHelperTemplate
448+
content, err := templates.TemplateFS.ReadFile("files/" + tmplInfo.Template)
449+
if err != nil {
450+
return "", fmt.Errorf("reading json merge helper template: %w", err)
451+
}
452+
453+
tmpl, err := template.New(tmplInfo.Name).Parse(string(content))
454+
if err != nil {
455+
return "", fmt.Errorf("parsing json merge helper template: %w", err)
456+
}
457+
458+
var result strings.Builder
459+
if err := tmpl.Execute(&result, nil); err != nil {
460+
return "", fmt.Errorf("executing json merge helper template: %w", err)
461+
}
462+
463+
return result.String(), nil
464+
}
465+
442466
// generateParamFunctionsFromContext generates the parameter styling/binding functions based on CodegenContext usage.
443467
// typesPrefix is prepended to Date/DateFormat references in param templates; empty when embedded.
444468
func generateParamFunctionsFromContext(ctx *CodegenContext, typesPrefix string) (string, error) {
@@ -1012,17 +1036,29 @@ func generateAnyOfType(gen *TypeGenerator, desc *SchemaDescriptor) string {
10121036
return ""
10131037
}
10141038

1015-
// anyOf types only need encoding/json (not fmt like oneOf)
1016-
gen.AddJSONImport()
1017-
1018-
doc := extractDescription(desc.Schema)
1019-
code := GenerateUnionType(desc.ShortName, members, false, doc)
1039+
gen.AddJSONImports()
10201040

1021-
marshalCode := GenerateUnionMarshalAnyOf(desc.ShortName, members)
1022-
unmarshalCode := GenerateUnionUnmarshalAnyOf(desc.ShortName, members)
1023-
applyDefaultsCode := GenerateUnionApplyDefaults(desc.ShortName, members)
1041+
cfg := UnionTypeConfig{
1042+
TypeName: desc.ShortName,
1043+
Members: members,
1044+
IsOneOf: false,
1045+
Doc: extractDescription(desc.Schema),
1046+
Discriminator: desc.Discriminator,
1047+
HelperPrefix: gen.helperPrefix(),
1048+
}
10241049

1025-
code += "\n" + marshalCode + "\n" + unmarshalCode + "\n" + applyDefaultsCode
1050+
code := GenerateUnionType(cfg)
1051+
code += "\n" + GenerateUnionAccessors(cfg)
1052+
if discCode := GenerateUnionDiscriminator(cfg); discCode != "" {
1053+
gen.AddImport("errors")
1054+
code += "\n" + discCode
1055+
}
1056+
code += "\n" + GenerateUnionMarshalUnmarshal(cfg)
1057+
if len(cfg.FixedFields) > 0 {
1058+
gen.AddImport("fmt")
1059+
}
1060+
gen.NeedHelper("json_merge")
1061+
code += "\n" + GenerateUnionApplyDefaults(desc.ShortName)
10261062

10271063
return code
10281064
}
@@ -1034,17 +1070,29 @@ func generateOneOfType(gen *TypeGenerator, desc *SchemaDescriptor) string {
10341070
return ""
10351071
}
10361072

1037-
// Union types need encoding/json and fmt for marshal/unmarshal
10381073
gen.AddJSONImports()
10391074

1040-
doc := extractDescription(desc.Schema)
1041-
code := GenerateUnionType(desc.ShortName, members, true, doc)
1042-
1043-
marshalCode := GenerateUnionMarshalOneOf(desc.ShortName, members)
1044-
unmarshalCode := GenerateUnionUnmarshalOneOf(desc.ShortName, members)
1045-
applyDefaultsCode := GenerateUnionApplyDefaults(desc.ShortName, members)
1075+
cfg := UnionTypeConfig{
1076+
TypeName: desc.ShortName,
1077+
Members: members,
1078+
IsOneOf: true,
1079+
Doc: extractDescription(desc.Schema),
1080+
Discriminator: desc.Discriminator,
1081+
HelperPrefix: gen.helperPrefix(),
1082+
}
10461083

1047-
code += "\n" + marshalCode + "\n" + unmarshalCode + "\n" + applyDefaultsCode
1084+
code := GenerateUnionType(cfg)
1085+
code += "\n" + GenerateUnionAccessors(cfg)
1086+
if discCode := GenerateUnionDiscriminator(cfg); discCode != "" {
1087+
gen.AddImport("errors")
1088+
code += "\n" + discCode
1089+
}
1090+
code += "\n" + GenerateUnionMarshalUnmarshal(cfg)
1091+
if len(cfg.FixedFields) > 0 {
1092+
gen.AddImport("fmt")
1093+
}
1094+
gen.NeedHelper("json_merge")
1095+
code += "\n" + GenerateUnionApplyDefaults(desc.ShortName)
10481096

10491097
return code
10501098
}
@@ -1095,17 +1143,27 @@ func collectUnionMembers(gen *TypeGenerator, parentDesc *SchemaDescriptor, membe
10951143
}
10961144
}
10971145

1146+
// Build reverse mapping: $ref path → []discriminator values
1147+
refToDiscValues := make(map[string][]string)
1148+
if parentDesc != nil && parentDesc.Discriminator != nil {
1149+
for discValue, refPath := range parentDesc.Discriminator.Mapping {
1150+
refToDiscValues[refPath] = append(refToDiscValues[refPath], discValue)
1151+
}
1152+
}
1153+
10981154
for i, proxy := range memberProxies {
10991155
var memberType string
1100-
var fieldName string
1156+
var methodName string
11011157
var hasApplyDefaults bool
1158+
var discValues []string
11021159

11031160
if proxy.IsReference() {
11041161
ref := proxy.GetReference()
11051162
if target, ok := gen.schemaIndex[ref]; ok {
11061163
memberType = target.ShortName
1107-
fieldName = target.ShortName
1164+
methodName = target.ShortName
11081165
hasApplyDefaults = schemaHasApplyDefaults(target.Schema)
1166+
discValues = refToDiscValues[ref]
11091167
} else {
11101168
continue
11111169
}
@@ -1124,23 +1182,23 @@ func collectUnionMembers(gen *TypeGenerator, parentDesc *SchemaDescriptor, membe
11241182

11251183
if desc, ok := descByPath[memberPath.String()]; ok && desc.ShortName != "" {
11261184
memberType = desc.ShortName
1127-
fieldName = desc.ShortName
1185+
methodName = desc.ShortName
11281186
hasApplyDefaults = schemaHasApplyDefaults(desc.Schema)
11291187
} else {
11301188
// This is a primitive type that doesn't have a named type
11311189
goType := gen.goTypeForSchema(schema, nil)
11321190
memberType = goType
1133-
// Create a field name based on the type
1134-
fieldName = gen.converter.ToTypeName(goType) + fmt.Sprintf("%d", i)
1191+
methodName = gen.converter.ToTypeName(goType) + fmt.Sprintf("%d", i)
11351192
hasApplyDefaults = false // Primitive types don't have ApplyDefaults
11361193
}
11371194
}
11381195

11391196
members = append(members, UnionMember{
1140-
FieldName: fieldName,
1141-
TypeName: memberType,
1142-
Index: i,
1143-
HasApplyDefaults: hasApplyDefaults,
1197+
TypeName: memberType,
1198+
MethodName: methodName,
1199+
Index: i,
1200+
HasApplyDefaults: hasApplyDefaults,
1201+
DiscriminatorValues: discValues,
11441202
})
11451203
}
11461204

experimental/codegen/internal/gather.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package codegen
33
import (
44
"fmt"
55
"log/slog"
6+
"strings"
67

78
"github.com/pb33f/libopenapi/datamodel/high/base"
89
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
@@ -619,6 +620,42 @@ func (g *gatherer) gatherFromSchema(schema *base.Schema, basePath SchemaPath, pa
619620
}
620621
}
621622

623+
// Extract discriminator information for oneOf/anyOf schemas
624+
if parent != nil && schema.Discriminator != nil && schema.Discriminator.PropertyName != "" {
625+
disc := schema.Discriminator
626+
info := &DiscriminatorInfo{
627+
PropertyName: disc.PropertyName,
628+
Mapping: make(map[string]string),
629+
}
630+
631+
if disc.Mapping != nil && disc.Mapping.Len() > 0 {
632+
// Explicit mapping from spec
633+
info.IsExplicit = true
634+
for pair := disc.Mapping.First(); pair != nil; pair = pair.Next() {
635+
info.Mapping[pair.Key()] = pair.Value()
636+
}
637+
} else {
638+
// Implicit mapping: infer from $ref schema names
639+
info.IsExplicit = false
640+
members := schema.OneOf
641+
if len(members) == 0 {
642+
members = schema.AnyOf
643+
}
644+
for _, proxy := range members {
645+
if proxy.IsReference() {
646+
ref := proxy.GetReference()
647+
parts := strings.Split(ref, "/")
648+
if len(parts) > 0 {
649+
schemaName := parts[len(parts)-1]
650+
info.Mapping[schemaName] = ref
651+
}
652+
}
653+
}
654+
}
655+
656+
parent.Discriminator = info
657+
}
658+
622659
// AdditionalProperties (if it's a schema, not a boolean)
623660
if schema.AdditionalProperties != nil && schema.AdditionalProperties.A != nil {
624661
addPropsPath := basePath.Append("additionalProperties")

0 commit comments

Comments
 (0)