From a3da996cf10dbb99ccd532f1af32b165c6948987 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Sun, 12 Jul 2026 17:04:27 -0700 Subject: [PATCH] Map format: duration to the runtime's RFC 3339 Duration type Closes: #2456 A `type: string, format: duration` schema previously fell through to a plain Go string, leaving parsing and validation of the OpenAPI "duration" format entirely to the user. The default type mapping now resolves it to openapi_types.Duration, the RFC 3339 duration type added to the runtime in v1.5.0, alongside the other openapi_types formats (date, uuid, email, binary). This changes generated types for specs already using format: duration. The old behavior can be restored by explicitly mapping the format back to a plain string in the configuration file: output-options: type-mapping: string: formats: duration: type: string Co-Authored-By: Claude Fable 5 --- pkg/codegen/typemapping.go | 1 + pkg/codegen/typemapping_test.go | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/pkg/codegen/typemapping.go b/pkg/codegen/typemapping.go index 2a54695f68..b47b63bf6b 100644 --- a/pkg/codegen/typemapping.go +++ b/pkg/codegen/typemapping.go @@ -99,6 +99,7 @@ var DefaultTypeMapping = TypeMapping{ "email": {Type: "openapi_types.Email"}, "date": {Type: "openapi_types.Date"}, "date-time": {Type: "time.Time", Import: "time"}, + "duration": {Type: "openapi_types.Duration"}, "json": {Type: "json.RawMessage", Import: "encoding/json"}, "uuid": {Type: "openapi_types.UUID"}, "binary": {Type: "openapi_types.File"}, diff --git a/pkg/codegen/typemapping_test.go b/pkg/codegen/typemapping_test.go index ca593cfd5d..cb54d543fc 100644 --- a/pkg/codegen/typemapping_test.go +++ b/pkg/codegen/typemapping_test.go @@ -3,7 +3,9 @@ package codegen import ( "testing" + "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFormatMapping_Resolve(t *testing.T) { @@ -81,8 +83,61 @@ func TestDefaultTypeMapping_Completeness(t *testing.T) { assert.Equal(t, "openapi_types.Email", dm.String.Resolve("email").Type) assert.Equal(t, "openapi_types.Date", dm.String.Resolve("date").Type) assert.Equal(t, "time.Time", dm.String.Resolve("date-time").Type) + assert.Equal(t, "openapi_types.Duration", dm.String.Resolve("duration").Type) assert.Equal(t, "json.RawMessage", dm.String.Resolve("json").Type) assert.Equal(t, "openapi_types.UUID", dm.String.Resolve("uuid").Type) assert.Equal(t, "openapi_types.File", dm.String.Resolve("binary").Type) assert.Equal(t, "string", dm.String.Resolve("unknown").Type) } + +// TestDurationFormatMapping verifies that `type: string, format: duration` +// generates the openapi_types.Duration runtime type by default, and that the +// historical plain-string behavior is restored by explicitly mapping the +// format back to string via type-mapping. +// Please see https://github.com/oapi-codegen/oapi-codegen/issues/2456 +func TestDurationFormatMapping(t *testing.T) { + const spec = ` +openapi: "3.0.0" +info: + version: 1.0.0 + title: Durations +paths: {} +components: + schemas: + RetryPolicy: + type: object + required: [backoff] + properties: + backoff: + type: string + format: duration +` + loader := openapi3.NewLoader() + swagger, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + opts := Configuration{ + PackageName: "api", + Generate: GenerateOptions{Models: true}, + OutputOptions: OutputOptions{SkipPrune: true}, + } + + code, err := Generate(swagger, opts) + require.NoError(t, err) + assert.Contains(t, code, "Backoff openapi_types.Duration `json:\"backoff\"`") + assert.Contains(t, code, `openapi_types "github.com/oapi-codegen/runtime/types"`) + + // The pre-duration behavior is a plain string; restore it by mapping + // the format back explicitly. + opts.OutputOptions.TypeMapping = &TypeMapping{ + String: FormatMapping{ + Formats: map[string]SimpleTypeSpec{ + "duration": {Type: "string"}, + }, + }, + } + code, err = Generate(swagger, opts) + require.NoError(t, err) + assert.Contains(t, code, "Backoff string `json:\"backoff\"`") + assert.NotContains(t, code, "openapi_types.Duration") +}