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

Commit 563faea

Browse files
committed
Simplify struct tag overrides
1 parent da2a1ef commit 563faea

File tree

3 files changed

+85
-55
lines changed

3 files changed

+85
-55
lines changed

experimental/Configuration.md

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -277,19 +277,21 @@ content-type-short-names:
277277
# - "^text/xml$"
278278

279279
# Struct tags: controls which struct tags are generated and their format.
280-
# Uses Go text/template syntax. If specified, completely replaces the defaults.
281-
# Default: json and form tags.
280+
# Uses Go text/template syntax with a simple 2-field context (.FieldName, .IsOptional).
281+
# Default: json and form tags with omitempty for optional fields.
282+
# User tags are merged by name: matching defaults are overridden, new tags are appended.
283+
# Extension-driven concerns (omitzero, json-ignore, omitempty overrides) are handled
284+
# automatically as post-processing — templates only need the simple context.
282285
struct-tags:
283286
tags:
284-
- name: json
285-
template: '{{if .JSONIgnore}}-{{else}}{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}{{if .OmitZero}},omitzero{{end}}{{end}}'
286-
- name: form
287-
template: '{{if .JSONIgnore}}-{{else}}{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}{{end}}'
288-
# Add additional tags:
287+
# Add additional tags (json and form defaults are kept):
289288
- name: yaml
290-
template: '{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}'
289+
template: '{{ .FieldName }}{{if .IsOptional}},omitempty{{end}}'
291290
- name: db
292291
template: '{{ .FieldName }}'
292+
# Can override the default json template too:
293+
# - name: json
294+
# template: '{{ .FieldName }}'
293295
```
294296

295297
## Struct tag template variables
@@ -298,11 +300,7 @@ The struct tag templates have access to the following fields:
298300

299301
| Variable | Type | Description |
300302
|----------|------|-------------|
301-
| `.FieldName` | `string` | The original field name from the OpenAPI spec |
302-
| `.GoFieldName` | `string` | The Go identifier name after name mangling |
303-
| `.IsOptional` | `bool` | Whether the field is optional in the schema |
304-
| `.IsNullable` | `bool` | Whether the field is nullable |
305-
| `.IsPointer` | `bool` | Whether the field is rendered as a pointer type |
306-
| `.OmitEmpty` | `bool` | Whether `omitempty` should be applied (from extensions or optionality) |
307-
| `.OmitZero` | `bool` | Whether `omitzero` should be applied (from `x-oapi-codegen-omitzero` extension) |
308-
| `.JSONIgnore` | `bool` | Whether the field should be ignored in JSON (from `x-go-json-ignore` extension) |
303+
| `.FieldName` | `string` | The original property name from the OpenAPI spec |
304+
| `.IsOptional` | `bool` | Whether the field is optional (not required) |
305+
306+
Extension-driven concerns (`x-oapi-codegen-omitzero`, `x-go-json-ignore`, `x-oapi-codegen-omitempty` overrides) are handled automatically as post-processing on the `json` and `form` tags. Templates do not need to handle these cases.

experimental/codegen/internal/output.go

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -178,27 +178,37 @@ func GenerateStruct(name string, fields []StructField, doc string, tagGen *Struc
178178
}
179179

180180
// generateFieldTag generates the struct tag for a field.
181+
// All tags go through the template engine with a simple 2-field context.
182+
// Extension-driven overrides (JSONIgnore, OmitZero, OmitEmpty) are applied
183+
// as post-processing on the resulting tag map.
181184
func generateFieldTag(f StructField, tagGen *StructTagGenerator) string {
182-
if tagGen == nil {
183-
if f.JSONIgnore {
184-
return "`json:\"-\"`"
185+
info := StructTagInfo{
186+
FieldName: f.JSONName,
187+
IsOptional: !f.Required,
188+
}
189+
190+
// All tags through the same template engine
191+
tags := tagGen.GenerateTagsMap(info)
192+
193+
// Extension overrides on well-known tags
194+
if f.JSONIgnore {
195+
tags["json"] = "-"
196+
tags["form"] = "-"
197+
} else {
198+
// OmitEmpty extension override: if extensions explicitly set OmitEmpty
199+
// differently from the schema default (!f.Required), adjust tags.
200+
if f.OmitEmpty != !f.Required {
201+
applyOmitEmptyOverride(tags, f.JSONName, f.OmitEmpty, "json", "form")
185202
}
186-
if f.OmitEmpty {
187-
return fmt.Sprintf("`json:\"%s,omitempty\"`", f.JSONName)
203+
// OmitZero (json-specific)
204+
if f.OmitZero {
205+
if v, ok := tags["json"]; ok {
206+
tags["json"] = v + ",omitzero"
207+
}
188208
}
189-
return fmt.Sprintf("`json:\"%s\"`", f.JSONName)
190209
}
191-
info := StructTagInfo{
192-
FieldName: f.JSONName,
193-
GoFieldName: f.Name,
194-
IsOptional: !f.Required,
195-
IsNullable: f.Nullable,
196-
IsPointer: f.Pointer,
197-
OmitEmpty: f.OmitEmpty,
198-
OmitZero: f.OmitZero,
199-
JSONIgnore: f.JSONIgnore,
200-
}
201-
return tagGen.GenerateTags(info)
210+
211+
return FormatTagsMap(tags)
202212
}
203213

204214
// GenerateStructWithAdditionalProps generates a struct with AdditionalProperties field

experimental/codegen/internal/structtags.go

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,21 @@ import (
88
)
99

1010
// StructTagInfo contains the data available to struct tag templates.
11+
// Extension-driven concerns (omitzero, json-ignore, omitempty overrides)
12+
// are applied as post-processing in generateFieldTag, not exposed here.
1113
type StructTagInfo struct {
1214
// FieldName is the JSON/YAML field name (from the OpenAPI property name)
1315
FieldName string
14-
// GoFieldName is the Go struct field name
15-
GoFieldName string
1616
// IsOptional is true if the field is optional (not required)
1717
IsOptional bool
18-
// IsNullable is true if the field can be null
19-
IsNullable bool
20-
// IsPointer is true if the Go type is a pointer
21-
IsPointer bool
22-
// OmitEmpty is true if the omitempty tag option should be used
23-
// (derived from IsOptional but can be overridden via extensions)
24-
OmitEmpty bool
25-
// OmitZero is true if the omitzero tag option should be used (Go 1.24+)
26-
OmitZero bool
27-
// JSONIgnore is true if the field should be excluded from JSON (json:"-")
28-
JSONIgnore bool
2918
}
3019

3120
// StructTagTemplate defines a single struct tag with a name and template.
3221
type StructTagTemplate struct {
3322
// Name is the tag name (e.g., "json", "yaml", "form")
3423
Name string `yaml:"name"`
3524
// Template is a Go text/template that produces the tag value.
36-
// Available fields: .FieldName, .GoFieldName, .IsOptional, .IsNullable, .IsPointer
25+
// Available fields: .FieldName, .IsOptional
3726
// Example: `{{ .FieldName }}{{if .IsOptional}},omitempty{{end}}`
3827
Template string `yaml:"template"`
3928
}
@@ -46,29 +35,48 @@ type StructTagsConfig struct {
4635
}
4736

4837
// DefaultStructTagsConfig returns the default struct tag configuration.
49-
// By default, json and form tags are generated.
38+
// By default, json and form tags are generated. Extension-driven concerns
39+
// (omitzero, json-ignore, omitempty overrides) are handled by post-processing
40+
// in generateFieldTag, not in the templates.
5041
func DefaultStructTagsConfig() StructTagsConfig {
5142
return StructTagsConfig{
5243
Tags: []StructTagTemplate{
5344
{
5445
Name: "json",
55-
Template: `{{if .JSONIgnore}}-{{else}}{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}{{if .OmitZero}},omitzero{{end}}{{end}}`,
46+
Template: `{{ .FieldName }}{{if .IsOptional}},omitempty{{end}}`,
5647
},
5748
{
5849
Name: "form",
59-
Template: `{{if .JSONIgnore}}-{{else}}{{ .FieldName }}{{if .OmitEmpty}},omitempty{{end}}{{end}}`,
50+
Template: `{{ .FieldName }}{{if .IsOptional}},omitempty{{end}}`,
6051
},
6152
},
6253
}
6354
}
6455

65-
// Merge merges user config on top of this config.
66-
// If user specifies any tags, they completely replace the defaults.
56+
// Merge merges user config on top of this config by name.
57+
// User entries override matching defaults; new entries are appended.
6758
func (c StructTagsConfig) Merge(other StructTagsConfig) StructTagsConfig {
68-
if len(other.Tags) > 0 {
69-
return other
59+
if len(other.Tags) == 0 {
60+
return c
7061
}
71-
return c
62+
// Start with defaults, override/append from user config
63+
merged := make(map[string]StructTagTemplate, len(c.Tags)+len(other.Tags))
64+
order := make([]string, 0, len(c.Tags)+len(other.Tags))
65+
for _, t := range c.Tags {
66+
merged[t.Name] = t
67+
order = append(order, t.Name)
68+
}
69+
for _, t := range other.Tags {
70+
if _, exists := merged[t.Name]; !exists {
71+
order = append(order, t.Name)
72+
}
73+
merged[t.Name] = t
74+
}
75+
result := StructTagsConfig{Tags: make([]StructTagTemplate, 0, len(order))}
76+
for _, name := range order {
77+
result.Tags = append(result.Tags, merged[name])
78+
}
79+
return result
7280
}
7381

7482
// StructTagGenerator generates struct tags from templates.
@@ -170,3 +178,17 @@ func FormatTagsMap(tags map[string]string) string {
170178

171179
return "`" + strings.Join(parts, " ") + "`"
172180
}
181+
182+
// applyOmitEmptyOverride rebuilds tag values when extensions override the default omitempty.
183+
func applyOmitEmptyOverride(tags map[string]string, fieldName string, omitEmpty bool, tagNames ...string) {
184+
for _, name := range tagNames {
185+
if _, ok := tags[name]; !ok {
186+
continue
187+
}
188+
v := fieldName
189+
if omitEmpty {
190+
v += ",omitempty"
191+
}
192+
tags[name] = v
193+
}
194+
}

0 commit comments

Comments
 (0)