Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion configuration-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,30 @@
},
"yaml-tags": {
"type": "boolean",
"description": "Enable the generation of YAML tags for struct fields"
"description": "Enable the generation of YAML tags for struct fields. Superseded by `struct-tags`: when a `yaml` entry is present there, this flag has no effect."
},
"struct-tags": {
"type": "object",
"description": "Configure which struct tags are generated on struct fields and how their values are rendered. Entries are merged by name on top of the defaults (json, form), so a `json` entry replaces the default template while new names add tags. Tags render in alphabetical order.",
"properties": {
"tags": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The struct tag name, e.g. `json`, `yaml`, `db`"
},
"template": {
"type": "string",
"description": "Go text/template producing the tag value. Available variables: `.FieldName`, `.IsOptional`, `.OmitEmpty`, `.OmitZero`, `.NeedsFormTag`. A template that renders to the empty string suppresses the tag on that field."
}
},
"required": ["name", "template"]
}
}
}
},
"client-response-bytes-function": {
"type": "boolean",
Expand Down
17 changes: 17 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,24 @@ output-options:
json: { type: json.RawMessage, import: encoding/json }
uuid: { type: openapi_types.UUID }
binary: { type: openapi_types.File }
# Superseded by struct-tags: a `yaml` entry there takes precedence over this flag
yaml-tags: false
# Configure generated struct tags. Entries are merged by name on top of the
# defaults shown below, so redefining `json` replaces its template while new
# names add tags. Values are Go text/templates over the variables
# .FieldName, .IsOptional, .OmitEmpty, .OmitZero and .NeedsFormTag; a
# template that renders empty suppresses the tag on that field. Tags are
# emitted in alphabetical order. Per-field x-oapi-codegen-extra-tags and
# x-go-json-ignore are applied on top of the rendered tags.
struct-tags:
tags:
- name: json
template: '{{.FieldName}}{{if .OmitEmpty}},omitempty{{end}}{{if .OmitZero}},omitzero{{end}}'
- name: form
template: '{{if .NeedsFormTag}}{{.FieldName}}{{if .OmitEmpty}},omitempty{{end}}{{end}}'
# Additional tags can be added, e.g.:
# - name: db
# template: '{{.FieldName}}'
client-response-bytes-function: false
skip-client-response-content-type: false
skip-response-body-getters: false
Expand Down
21 changes: 21 additions & 0 deletions internal/test/options/struct_tags/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# yaml-language-server: $schema=../../../../configuration-schema.json
package: optionsstructtags
output: struct_tags.gen.go
generate:
models: true
output-options:
skip-prune: true
# yaml-tags is superseded by the explicit yaml entry in struct-tags below:
# the entry's template wins over the flag's injected default.
yaml-tags: true
# struct-tags: user-configurable struct tag generation. Entries merge by
# name on top of the defaults (json, form), so the yaml entry overrides
# the yaml-tags default while validate/db add new tags.
struct-tags:
tags:
- name: yaml
template: '{{.FieldName}}'
- name: validate
template: '{{if not .IsOptional}}required{{end}}'
- name: db
template: '{{.FieldName}}'
8 changes: 8 additions & 0 deletions internal/test/options/struct_tags/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Package optionsstructtags exercises the struct-tags output option: struct
// tag generation is user-configurable via Go text/templates. The config for
// this package overrides the yaml tag template (superseding the yaml-tags
// flag), and adds validate and db tags, while keeping the default json and
// form templates.
package optionsstructtags

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml
45 changes: 45 additions & 0 deletions internal/test/options/struct_tags/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# ==========================================================================
# Category: options/struct_tags
# Tests: struct-tags output option: user-configurable struct tag templates.
# - default json template kept when not overridden
# - user yaml entry supersedes the yaml-tags flag (no omitempty)
# - added tags (validate, db) rendered on every field
# - form tag still gated on form-style parameters (NeedsFormTag)
# ==========================================================================
openapi: "3.0.1"
info:
title: options/struct_tags
version: "1.0.0"
description: |
Exercises the struct-tags output option, which makes struct tag
generation user-configurable via Go text/templates.
paths:
/pets:
get:
operationId: listPets
parameters:
# form-style query parameter: field must carry a form tag
- name: limit
description: Query parameter
in: query
required: false
schema:
type: integer
responses:
"200":
description: pets
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
components:
schemas:
Pet:
type: object
required:
- name
properties:
name:
type: string
tag:
type: string
16 changes: 16 additions & 0 deletions internal/test/options/struct_tags/struct_tags.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions internal/test/options/struct_tags/struct_tags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package optionsstructtags

import (
"reflect"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// fieldTag returns the struct tag of the named field.
func fieldTag(t *testing.T, typ reflect.Type, field string) reflect.StructTag {
t.Helper()
f, ok := typ.FieldByName(field)
require.True(t, ok, "field %s not found on %s", field, typ)
return f.Tag
}

func TestStructTags(t *testing.T) {
pet := reflect.TypeOf(Pet{})

t.Run("default json template is kept", func(t *testing.T) {
assert.Equal(t, "name", fieldTag(t, pet, "Name").Get("json"))
assert.Equal(t, "tag,omitempty", fieldTag(t, pet, "Tag").Get("json"))
})

t.Run("user yaml entry supersedes yaml-tags flag", func(t *testing.T) {
// The overridden template has no omitempty, unlike the yaml-tags default.
assert.Equal(t, "name", fieldTag(t, pet, "Name").Get("yaml"))
assert.Equal(t, "tag", fieldTag(t, pet, "Tag").Get("yaml"))
})

t.Run("added tags are rendered", func(t *testing.T) {
assert.Equal(t, "name", fieldTag(t, pet, "Name").Get("db"))
assert.Equal(t, "required", fieldTag(t, pet, "Name").Get("validate"))
// Empty render suppresses the tag entirely on optional fields.
_, hasValidate := fieldTag(t, pet, "Tag").Lookup("validate")
assert.False(t, hasValidate)
})

t.Run("form tag still gated on form-style parameters", func(t *testing.T) {
params := reflect.TypeOf(ListPetsParams{})
assert.Equal(t, "limit,omitempty", fieldTag(t, params, "Limit").Get("form"))
// Schema fields are not form-bound, so no form tag is emitted.
_, hasForm := fieldTag(t, pet, "Name").Lookup("form")
assert.False(t, hasForm)
})
}
25 changes: 25 additions & 0 deletions pkg/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ var globalState struct {
// streamingContentTypeRegexes are the compiled regexes (defaults + user)
// used by ResponseContentDefinition.IsStreamingContentType.
streamingContentTypeRegexes []*regexp.Regexp
// schemaFieldTagGenerator renders struct tags for schema property
// fields (defaults + output-options.struct-tags, including the legacy
// yaml-tags injection). Built in Generate; lazily built for direct
// callers via schemaFieldTagGenerator().
schemaFieldTagGenerator *structTagGenerator
// paramFieldTagGenerator renders struct tags for path parameter fields
// on strict RequestObject structs; identical to the schema generator
// except the legacy yaml-tags flag does not apply.
paramFieldTagGenerator *structTagGenerator
}

// goImport represents a go package to be imported in the generated code
Expand Down Expand Up @@ -166,6 +175,22 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) {
globalState.typeMapping = DefaultTypeMapping
}

// Build the struct tag generators eagerly so invalid user templates in
// output-options.struct-tags are reported as errors instead of being
// skipped. Assigning here also resets any state from a prior Generate.
schemaTagGen, err := newStructTagGenerator(
defaultStructTagsConfig(opts.OutputOptions.EnableYamlTags).Merge(opts.OutputOptions.StructTags))
if err != nil {
return "", fmt.Errorf("error in output-options.struct-tags: %w", err)
}
globalState.schemaFieldTagGenerator = schemaTagGen
paramTagGen, err := newStructTagGenerator(
defaultStructTagsConfig(false).Merge(opts.OutputOptions.StructTags))
if err != nil {
return "", fmt.Errorf("error in output-options.struct-tags: %w", err)
}
globalState.paramFieldTagGenerator = paramTagGen

filterOperationsByTag(spec, opts)
filterOperationsByOperationID(spec, opts)
if !opts.OutputOptions.SkipPrune {
Expand Down
18 changes: 17 additions & 1 deletion pkg/codegen/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,9 +433,19 @@ type OutputOptions struct {
// Overlay defines configuration for the OpenAPI Overlay (https://github.com/OAI/Overlay-Specification) to manipulate the OpenAPI specification before generation. This allows modifying the specification without needing to apply changes directly to it, making it easier to keep it up-to-date.
Overlay OutputOptionsOverlay `yaml:"overlay"`

// EnableYamlTags adds YAML tags to generated structs, in addition to default JSON ones
// EnableYamlTags adds YAML tags to generated structs, in addition to default JSON ones.
// Superseded by StructTags: when a `yaml` entry is present in `struct-tags`, this flag
// has no effect.
EnableYamlTags bool `yaml:"yaml-tags,omitempty"`

// StructTags configures which struct tags are generated on struct fields and how their
// values are rendered. Each entry is a tag name plus a Go text/template evaluated
// against the field (see StructTagInfo for the available variables). Entries are
// merged by name on top of the defaults, so a `json` entry replaces the default json
// template while new names add tags. Tags render in alphabetical order; a template
// that renders to the empty string suppresses that tag on that field.
StructTags StructTagsConfig `yaml:"struct-tags,omitempty"`

// ClientResponseBytesFunction decides whether to enable the generation of a `Bytes()` method on response objects for `ClientWithResponses`
ClientResponseBytesFunction bool `yaml:"client-response-bytes-function,omitempty"`

Expand Down Expand Up @@ -509,6 +519,12 @@ func (oo OutputOptions) Validate() map[string]string {
}
}

if _, err := newStructTagGenerator(defaultStructTagsConfig(oo.EnableYamlTags).Merge(oo.StructTags)); err != nil {
return map[string]string{
"struct-tags": err.Error(),
}
}

return nil
}

Expand Down
20 changes: 10 additions & 10 deletions pkg/codegen/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,19 @@ func (pd ParameterDefinition) ZeroValueIsNil() bool {
return strings.HasPrefix(pd.Schema.GoType, "map[")
}

// JsonTag generates the JSON annotation to map GoType to json type name. If Parameter
// Foo is marshaled to json as "foo", this will create the annotation
// 'json:"foo"'
// JsonTag generates the struct tag annotation for a parameter field on a
// strict RequestObject struct. If Parameter Foo is marshaled to json as "foo",
// this will create the annotation 'json:"foo"'. Tags configured via
// output-options.struct-tags are rendered too (the legacy yaml-tags flag does
// not apply to these fields).
// It also includes any additional struct tags from x-oapi-codegen-extra-tags
// at the parameter or schema level (parameter-level takes precedence).
func (pd *ParameterDefinition) JsonTag() string {
fieldTags := make(map[string]string)

if pd.Required {
fieldTags["json"] = pd.ParamName
} else {
fieldTags["json"] = pd.ParamName + ",omitempty"
}
fieldTags := paramFieldTagGenerator().generateTagsMap(StructTagInfo{
FieldName: pd.ParamName,
IsOptional: !pd.Required,
OmitEmpty: !pd.Required,
})

// Merge x-oapi-codegen-extra-tags from schema level first, then parameter level
// so that parameter-level takes precedence.
Expand Down
26 changes: 7 additions & 19 deletions pkg/codegen/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -1219,13 +1219,6 @@ type FieldDescriptor struct {
IsRef bool // Is this schema a reference to predefined object?
}

func stringOrEmpty(b bool, s string) string {
if b {
return s
}
return ""
}

// GenFieldsFromProperties produce corresponding field names with JSON annotations,
// given a list of schema descriptors
func GenFieldsFromProperties(props []Property) []string {
Expand Down Expand Up @@ -1292,18 +1285,13 @@ func GenFieldsFromProperties(props []Property) []string {
}
}

fieldTags := make(map[string]string)

fieldTags["json"] = p.JsonFieldName +
stringOrEmpty(omitEmpty, ",omitempty") +
stringOrEmpty(omitZero, ",omitzero")

if globalState.options.OutputOptions.EnableYamlTags {
fieldTags["yaml"] = p.JsonFieldName + stringOrEmpty(omitEmpty, ",omitempty")
}
if p.NeedsFormTag {
fieldTags["form"] = p.JsonFieldName + stringOrEmpty(omitEmpty, ",omitempty")
}
fieldTags := schemaFieldTagGenerator().generateTagsMap(StructTagInfo{
FieldName: p.JsonFieldName,
IsOptional: !p.Required,
OmitEmpty: omitEmpty,
OmitZero: omitZero,
NeedsFormTag: p.NeedsFormTag,
})

// Support x-go-json-ignore
if extension, ok := p.Extensions[extPropGoJsonIgnore]; ok {
Expand Down
Loading