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
59 changes: 58 additions & 1 deletion configuration-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,25 @@
"type": "boolean",
"description": "When set to true, automatically renames types that collide across different OpenAPI component sections (schemas, parameters, requestBodies, responses, headers) by appending a suffix based on the component section (e.g., 'Parameter', 'Response', 'RequestBody'). Without this, the codegen will error on duplicate type names, requiring manual resolution via x-go-name.",
"default": false
},
"type-mapping": {
"type": "object",
"additionalProperties": false,
"description": "TypeMapping allows customizing OpenAPI type/format to Go type mappings. User-specified mappings are merged on top of the defaults, so you only need to specify the types you want to override.",
"properties": {
"integer": {
"$ref": "#/$defs/format-mapping"
},
"number": {
"$ref": "#/$defs/format-mapping"
},
"boolean": {
"$ref": "#/$defs/format-mapping"
},
"string": {
"$ref": "#/$defs/format-mapping"
}
}
}
}
},
Expand Down Expand Up @@ -294,5 +313,43 @@
"required": [
"package",
"output"
]
],
"$defs": {
"simple-type-spec": {
"type": "object",
"additionalProperties": false,
"description": "Specifies a Go type and optional import path",
"properties": {
"type": {
"type": "string",
"description": "The Go type to use (e.g. \"int64\", \"time.Time\", \"github.com/shopspring/decimal.Decimal\")"
},
"import": {
"type": "string",
"description": "The Go import path required for this type (e.g. \"time\", \"encoding/json\")"
}
},
"required": [
"type"
]
},
"format-mapping": {
"type": "object",
"additionalProperties": false,
"description": "Maps an OpenAPI type's formats to Go types",
"properties": {
"default": {
"$ref": "#/$defs/simple-type-spec",
"description": "The default Go type when no format is specified or the format is unrecognized"
},
"formats": {
"type": "object",
"description": "Format-specific Go type overrides (e.g. \"int32\": {\"type\": \"int32\"}, \"double\": {\"type\": \"float64\"})",
"additionalProperties": {
"$ref": "#/$defs/simple-type-spec"
}
}
}
}
}
}
14 changes: 14 additions & 0 deletions examples/output-options/type-mapping/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# yaml-language-server: $schema=../../configuration-schema.json
package: typemapping
generate:
models: true
output-options:
skip-prune: true
type-mapping:
number:
default:
type: int64
formats:
date:
type: CustomDateHandler
output: typemapping.gen.go
3 changes: 3 additions & 0 deletions examples/output-options/type-mapping/customdate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package typemapping

type CustomDateHandler struct{}
6 changes: 6 additions & 0 deletions examples/output-options/type-mapping/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package typemapping

// The configuration in this directory overrides the default handling of
// "type: number" from producing an `int` to producing an `int64`, and we
// override `type: string, format: date` to be a custom type in this package.
//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml spec.yaml
18 changes: 18 additions & 0 deletions examples/output-options/type-mapping/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
openapi: "3.0.1"
info:
version: 1.0.0
title: Type mapping test
paths: {}
components:
schemas:
EmployeeDatabaseRecord:
type: object
required:
- ID
- DateHired
properties:
ID:
type: number
DateHired:
type: number
format: date
10 changes: 10 additions & 0 deletions examples/output-options/type-mapping/typemapping.gen.go

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

7 changes: 7 additions & 0 deletions pkg/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ var globalState struct {
// initialismsMap stores initialisms as "lower(initialism) -> initialism" map.
// List of initialisms was taken from https://staticcheck.io/docs/configuration/options/#initialisms.
initialismsMap map[string]string
// typeMapping is the merged type mapping (defaults + user overrides).
typeMapping TypeMapping
}

// goImport represents a go package to be imported in the generated code
Expand Down Expand Up @@ -140,6 +142,11 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) {
globalState.options = opts
globalState.spec = spec
globalState.importMapping = constructImportMapping(opts.ImportMapping)
if opts.OutputOptions.TypeMapping != nil {
globalState.typeMapping = DefaultTypeMapping.Merge(*opts.OutputOptions.TypeMapping)
} else {
globalState.typeMapping = DefaultTypeMapping
}

filterOperationsByTag(spec, opts)
filterOperationsByOperationID(spec, opts)
Expand Down
4 changes: 4 additions & 0 deletions pkg/codegen/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,10 @@ type OutputOptions struct {
// "RequestBody"). Without this, the codegen will error on duplicate type
// names, requiring manual resolution via x-go-name.
ResolveTypeNameCollisions bool `yaml:"resolve-type-name-collisions,omitempty"`

// TypeMapping allows customizing OpenAPI type/format to Go type mappings.
// User-specified mappings are merged on top of the defaults.
TypeMapping *TypeMapping `yaml:"type-mapping,omitempty"`
}

func (oo OutputOptions) Validate() map[string]string {
Expand Down
60 changes: 12 additions & 48 deletions pkg/codegen/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,62 +629,26 @@ func oapiSchemaToGoType(schema *openapi3.Schema, path []string, outSchema *Schem
setSkipOptionalPointerForContainerType(outSchema)

} else if t.Is("integer") {
// We default to int if format doesn't ask for something else.
switch f {
case "int64",
"int32",
"int16",
"int8",
"int",
"uint64",
"uint32",
"uint16",
"uint8",
"uint":
outSchema.GoType = f
default:
outSchema.GoType = "int"
}
spec := globalState.typeMapping.Integer.Resolve(f)
outSchema.GoType = spec.Type
outSchema.DefineViaAlias = true
} else if t.Is("number") {
// We default to float for "number"
switch f {
case "double":
outSchema.GoType = "float64"
case "float", "":
outSchema.GoType = "float32"
default:
return fmt.Errorf("invalid number format: %s", f)
}
spec := globalState.typeMapping.Number.Resolve(f)
outSchema.GoType = spec.Type
outSchema.DefineViaAlias = true
} else if t.Is("boolean") {
if f != "" {
return fmt.Errorf("invalid format (%s) for boolean", f)
}
outSchema.GoType = "bool"
spec := globalState.typeMapping.Boolean.Resolve(f)
outSchema.GoType = spec.Type
outSchema.DefineViaAlias = true
} else if t.Is("string") {
// Special case string formats here.
switch f {
case "byte":
outSchema.GoType = "[]byte"
spec := globalState.typeMapping.String.Resolve(f)
outSchema.GoType = spec.Type
// Preserve special behaviors for specific types
if outSchema.GoType == "[]byte" {
setSkipOptionalPointerForContainerType(outSchema)
case "email":
outSchema.GoType = "openapi_types.Email"
case "date":
outSchema.GoType = "openapi_types.Date"
case "date-time":
outSchema.GoType = "time.Time"
case "json":
outSchema.GoType = "json.RawMessage"
}
if outSchema.GoType == "json.RawMessage" {
outSchema.SkipOptionalPointer = true
case "uuid":
outSchema.GoType = "openapi_types.UUID"
case "binary":
outSchema.GoType = "openapi_types.File"
default:
// All unrecognized formats are simply a regular string.
outSchema.GoType = "string"
}
outSchema.DefineViaAlias = true
} else {
Expand Down
109 changes: 109 additions & 0 deletions pkg/codegen/typemapping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package codegen

// SimpleTypeSpec defines the Go type for an OpenAPI type/format combination,
// along with any import required to use it.
type SimpleTypeSpec struct {
Type string `yaml:"type" json:"type"`
Import string `yaml:"import,omitempty" json:"import,omitempty"`
}

// FormatMapping defines the default Go type and format-specific overrides
// for an OpenAPI type.
type FormatMapping struct {
Default SimpleTypeSpec `yaml:"default" json:"default"`
Formats map[string]SimpleTypeSpec `yaml:"formats,omitempty" json:"formats,omitempty"`
}

// TypeMapping defines the mapping from OpenAPI types to Go types.
type TypeMapping struct {
Integer FormatMapping `yaml:"integer,omitempty" json:"integer,omitempty"`
Number FormatMapping `yaml:"number,omitempty" json:"number,omitempty"`
Boolean FormatMapping `yaml:"boolean,omitempty" json:"boolean,omitempty"`
String FormatMapping `yaml:"string,omitempty" json:"string,omitempty"`
}

// Merge returns a new TypeMapping with user overrides applied on top of base.
func (base TypeMapping) Merge(user TypeMapping) TypeMapping {
return TypeMapping{
Integer: base.Integer.merge(user.Integer),
Number: base.Number.merge(user.Number),
Boolean: base.Boolean.merge(user.Boolean),
String: base.String.merge(user.String),
}
}

func (base FormatMapping) merge(user FormatMapping) FormatMapping {
result := FormatMapping{
Default: base.Default,
Formats: make(map[string]SimpleTypeSpec),
}

// Copy base formats
for k, v := range base.Formats {
result.Formats[k] = v
}

// Override with user default if specified
if user.Default.Type != "" {
result.Default = user.Default
}

// Override/add user formats
for k, v := range user.Formats {
result.Formats[k] = v
}

return result
}

// Resolve returns the SimpleTypeSpec for a given format string.
// If the format has a specific mapping, that is returned; otherwise the default is used.
func (fm FormatMapping) Resolve(format string) SimpleTypeSpec {
if format != "" {
if spec, ok := fm.Formats[format]; ok {
return spec
}
}
return fm.Default
}

// DefaultTypeMapping provides the default OpenAPI type/format to Go type mappings.
var DefaultTypeMapping = TypeMapping{
Integer: FormatMapping{
Default: SimpleTypeSpec{Type: "int"},
Formats: map[string]SimpleTypeSpec{
"int": {Type: "int"},
"int8": {Type: "int8"},
"int16": {Type: "int16"},
"int32": {Type: "int32"},
"int64": {Type: "int64"},
"uint": {Type: "uint"},
"uint8": {Type: "uint8"},
"uint16": {Type: "uint16"},
"uint32": {Type: "uint32"},
"uint64": {Type: "uint64"},
},
},
Number: FormatMapping{
Default: SimpleTypeSpec{Type: "float32"},
Formats: map[string]SimpleTypeSpec{
"float": {Type: "float32"},
"double": {Type: "float64"},
},
},
Boolean: FormatMapping{
Default: SimpleTypeSpec{Type: "bool"},
},
String: FormatMapping{
Default: SimpleTypeSpec{Type: "string"},
Formats: map[string]SimpleTypeSpec{
"byte": {Type: "[]byte"},
"email": {Type: "openapi_types.Email"},
"date": {Type: "openapi_types.Date"},
"date-time": {Type: "time.Time", Import: "time"},
"json": {Type: "json.RawMessage", Import: "encoding/json"},
"uuid": {Type: "openapi_types.UUID"},
"binary": {Type: "openapi_types.File"},
},
},
}
Loading