Skip to content
Merged
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
7 changes: 7 additions & 0 deletions configuration-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@
"type": "boolean",
"description": "AlwaysPrefixEnumValues specifies whether to always prefix enum values with the schema name. Defaults to true."
},
"additional-tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Additional struct tags to emit on generated fields, mirroring the json tag value. For example, [\"yaml\"] emits yaml:\"field_name,omitempty\" alongside the json tag. Useful when structs are decoded by libraries that don't read json tags (e.g. go.yaml.in/yaml/v4). Per-property x-oapi-codegen-extra-tags take priority over these global tags."
},
"validation": {
"$ref": "#/definitions/ValidationOptions",
"description": "Validation specifies options for Validate() method generation."
Expand Down
26 changes: 26 additions & 0 deletions examples/extensions/additionaltags/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: additional-tags
components:
schemas:
Issue:
type: object
required:
- id
- closed_at
properties:
id:
type: integer
closed_at:
type: string
format: date-time
label_name:
type: string
extra_override:
type: string
x-oapi-codegen-extra-tags:
yaml: custom_override
ignored_field:
type: string
x-go-json-ignore: true
10 changes: 10 additions & 0 deletions examples/extensions/additionaltags/cfg.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# yaml-language-server: $schema=../../../configuration-schema.json
package: additionaltags
# to make sure that all types are generated, even if they're unreferenced
skip-prune: true
generate:
client: false
additional-tags:
- yaml
output:
use-single-file: true
29 changes: 29 additions & 0 deletions examples/extensions/additionaltags/gen.go

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

3 changes: 3 additions & 0 deletions examples/extensions/additionaltags/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package additionaltags

//go:generate go run github.com/doordash-oss/oapi-codegen-dd/v3/cmd/oapi-codegen -config cfg.yaml api.yaml
5 changes: 5 additions & 0 deletions pkg/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,15 @@ func CreateParseContextFromModel(model *v3high.Document, cfg Configuration) (*Pa
return nil, nil
}

if err := cfg.Generate.Validate(); err != nil {
return nil, err
}

parseOptions := ParseOptions{
OmitDescription: cfg.Generate.OmitDescription,
DefaultIntType: cfg.Generate.DefaultIntType,
AlwaysPrefixEnumValues: cfg.Generate.AlwaysPrefixEnumValues,
AdditionalTags: cfg.Generate.AdditionalTags,
SkipValidation: cfg.Generate.Validation.Skip,
ErrorMapping: cfg.ErrorMapping,
typeTracker: newTypeTracker(),
Expand Down
19 changes: 19 additions & 0 deletions pkg/codegen/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package codegen

import (
"fmt"
"strings"
"time"
)

Expand Down Expand Up @@ -170,6 +171,9 @@ func (o Configuration) OverwriteWith(other Configuration) Configuration {
if other.Generate.AlwaysPrefixEnumValues {
o.Generate.AlwaysPrefixEnumValues = other.Generate.AlwaysPrefixEnumValues
}
if len(other.Generate.AdditionalTags) > 0 {
o.Generate.AdditionalTags = other.Generate.AdditionalTags
}
// Overwrite Validation options
if other.Generate.Validation.Skip {
o.Generate.Validation.Skip = other.Generate.Validation.Skip
Expand Down Expand Up @@ -312,10 +316,25 @@ type GenerateOptions struct {
// AlwaysPrefixEnumValues specifies whether to always prefix enum values with the schema name. Defaults to true.
AlwaysPrefixEnumValues bool `yaml:"always-prefix-enum-values"`

// AdditionalTags specifies additional struct tags to emit on generated fields,
// mirroring the json tag value. For example, ["yaml"] emits yaml:"field_name,omitempty"
// alongside the json tag. Per-property x-oapi-codegen-extra-tags take priority.
AdditionalTags []string `yaml:"additional-tags"`

// Validation specifies options for Validate() method generation.
Validation ValidationOptions `yaml:"validation"`
}

// Validate returns an error if any generate options are invalid.
func (o GenerateOptions) Validate() error {
for _, tag := range o.AdditionalTags {
if tag == "" || strings.ContainsAny(tag, " \t\"'`:") {
return fmt.Errorf("%w: %q", ErrInvalidAdditionalTag, tag)
}
}
return nil
}

type ValidationOptions struct {
// Skip specifies whether to skip Validation method generation. Defaults to false.
Skip bool `yaml:"skip"`
Expand Down
32 changes: 32 additions & 0 deletions pkg/codegen/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,35 @@ func TestBasePathOverwriteWith(t *testing.T) {
assert.Equal(t, "/keep/this", result.BasePath)
})
}

func TestGenerateOptions_Validate(t *testing.T) {
t.Run("valid tags", func(t *testing.T) {
o := GenerateOptions{AdditionalTags: []string{"yaml", "toml", "mapstructure"}}
assert.NoError(t, o.Validate())
})

t.Run("empty tag name", func(t *testing.T) {
o := GenerateOptions{AdditionalTags: []string{""}}
assert.ErrorIs(t, o.Validate(), ErrInvalidAdditionalTag)
})

t.Run("tag with space", func(t *testing.T) {
o := GenerateOptions{AdditionalTags: []string{"my tag"}}
assert.ErrorIs(t, o.Validate(), ErrInvalidAdditionalTag)
})

t.Run("tag with colon", func(t *testing.T) {
o := GenerateOptions{AdditionalTags: []string{"bad:tag"}}
assert.ErrorIs(t, o.Validate(), ErrInvalidAdditionalTag)
})

t.Run("tag with quote", func(t *testing.T) {
o := GenerateOptions{AdditionalTags: []string{`bad"tag`}}
assert.ErrorIs(t, o.Validate(), ErrInvalidAdditionalTag)
})

t.Run("nil tags is valid", func(t *testing.T) {
o := GenerateOptions{}
assert.NoError(t, o.Validate())
})
}
1 change: 1 addition & 0 deletions pkg/codegen/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ var (
ErrHandlerKindUnsupported = errors.New("unsupported handler kind")
ErrServerHandlerPackageRequired = errors.New("server handler-package is required when server generation is enabled")
ErrServerRequiresService = errors.New("server generation requires service generation: add 'service: {}' to handler config")
ErrInvalidAdditionalTag = errors.New("invalid additional tag name: must not be empty or contain spaces, quotes, or colons")
)
1 change: 1 addition & 0 deletions pkg/codegen/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type ParseOptions struct {
OmitDescription bool
DefaultIntType string
AlwaysPrefixEnumValues bool
AdditionalTags []string
SkipValidation bool

// ErrorMapping maps response type names to the field that should be used
Expand Down
7 changes: 7 additions & 0 deletions pkg/codegen/schema_property.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,13 @@ func genFieldsFromProperties(props []Property, options ParseOptions) []string {
}
}

// Emit additional tags (mirrors json tag value, but doesn't overwrite extra-tags)
for _, tag := range options.AdditionalTags {
if _, exists := fieldTags[tag]; !exists {
fieldTags[tag] = fieldTags["json"]
}
}

// Support x-sensitive-data - add a simple marker tag
// The actual masking is handled via custom MarshalJSON generation
if _, ok := p.Extensions[extSensitiveData]; ok {
Expand Down
96 changes: 96 additions & 0 deletions pkg/codegen/schema_property_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,102 @@ func TestProperty_GoTypeDef(t *testing.T) {
}
}

func TestGenFieldsFromProperties_AdditionalTags(t *testing.T) {
tests := []struct {
name string
props []Property
options ParseOptions
contains []string
notContains []string
}{
{
name: "yaml tag mirrors json tag",
props: []Property{
{
GoName: "ClosedAt",
JsonFieldName: "closed_at",
Schema: GoSchema{GoType: "string"},
Constraints: Constraints{Required: ptr(true)},
},
},
options: ParseOptions{
AdditionalTags: []string{"yaml"},
SkipValidation: true,
},
contains: []string{`json:"closed_at"`, `yaml:"closed_at"`},
},
{
name: "yaml tag includes omitempty when nullable",
props: []Property{
{
GoName: "LabelName",
JsonFieldName: "label_name",
Schema: GoSchema{GoType: "*string"},
Constraints: Constraints{Nullable: ptr(true)},
},
},
options: ParseOptions{
AdditionalTags: []string{"yaml"},
SkipValidation: true,
},
contains: []string{`json:"label_name,omitempty"`, `yaml:"label_name,omitempty"`},
},
{
name: "extra-tags take priority over additional-tags",
props: []Property{
{
GoName: "ExtraOverride",
JsonFieldName: "extra_override",
Schema: GoSchema{GoType: "*string"},
Constraints: Constraints{Nullable: ptr(true)},
Extensions: map[string]any{
"x-oapi-codegen-extra-tags": map[string]any{
"yaml": "custom_override",
},
},
},
},
options: ParseOptions{
AdditionalTags: []string{"yaml"},
SkipValidation: true,
},
contains: []string{`json:"extra_override,omitempty"`, `yaml:"custom_override"`},
notContains: []string{`yaml:"extra_override,omitempty"`},
},
{
name: "json ignored fields get yaml ignored too",
props: []Property{
{
GoName: "IgnoredField",
JsonFieldName: "ignored_field",
Schema: GoSchema{GoType: "*string"},
Extensions: map[string]any{
"x-go-json-ignore": true,
},
},
},
options: ParseOptions{
AdditionalTags: []string{"yaml"},
SkipValidation: true,
},
contains: []string{`json:"-"`, `yaml:"-"`},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fields := genFieldsFromProperties(tt.props, tt.options)
assert.Len(t, fields, 1)
for _, s := range tt.contains {
assert.Contains(t, fields[0], s)
}
for _, s := range tt.notContains {
assert.NotContains(t, fields[0], s)
}
})
}
}

func TestProperty_GoTypeDef_nullable(t *testing.T) {
type fields struct {
GlobalStateDisableRequiredReadOnlyAsPointer bool
Expand Down
Loading