diff --git a/bind.go b/bind.go index b0331a88a..6d7e0f96f 100644 --- a/bind.go +++ b/bind.go @@ -418,6 +418,18 @@ func unmarshalInputsToField(valueKind reflect.Kind, values []string, field refle return true, unmarshaler.UnmarshalParams(values) } +// timeLayoutFromFormatTag maps a `format` struct tag to a time.Parse layout. +// OpenAPI format names (date-time, date-time-local) are RFC3339 so they do not +// collide with swag/openapi `format` tags. Any other value is a Go reference-time layout. +func timeLayoutFromFormatTag(formatTag string) string { + switch formatTag { + case "date-time", "date-time-local": + return time.RFC3339 + default: + return formatTag + } +} + func unmarshalInputToField(valueKind reflect.Kind, val string, field reflect.Value, formatTag string) (bool, error) { if valueKind == reflect.Pointer { if field.IsNil() { @@ -430,7 +442,7 @@ func unmarshalInputToField(valueKind reflect.Kind, val string, field reflect.Val // Handle time.Time with custom format tag if formatTag != "" { if _, isTime := fieldIValue.(*time.Time); isTime { - t, err := time.Parse(formatTag, val) + t, err := time.Parse(timeLayoutFromFormatTag(formatTag), val) if err != nil { return true, err } diff --git a/bind_test.go b/bind_test.go index 6a40985e9..fee27b215 100644 --- a/bind_test.go +++ b/bind_test.go @@ -1588,11 +1588,13 @@ func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file test func TestTimeFormatBinding(t *testing.T) { type TestStruct struct { - DateTimeLocal time.Time `form:"datetime_local" format:"2006-01-02T15:04"` - Date time.Time `query:"date" format:"2006-01-02"` - CustomFormat time.Time `form:"custom" format:"01/02/2006 15:04:05"` - DefaultTime time.Time `form:"default_time"` // No format tag - should use default parsing - PtrTime *time.Time `query:"ptr_time" format:"2006-01-02"` + DateTimeLocal time.Time `form:"datetime_local" format:"2006-01-02T15:04"` + Date time.Time `query:"date" format:"2006-01-02"` + CustomFormat time.Time `form:"custom" format:"01/02/2006 15:04:05"` + DefaultTime time.Time `form:"default_time"` // No format tag - should use default parsing + PtrTime *time.Time `query:"ptr_time" format:"2006-01-02"` + OpenAPIDateTime time.Time `form:"openapi_datetime" format:"date-time"` + OpenAPIDateTimeLocal time.Time `form:"openapi_datetime_local" format:"date-time-local"` } testCases := []struct { @@ -1628,6 +1630,22 @@ func TestTimeFormatBinding(t *testing.T) { CustomFormat: time.Date(2023, 12, 25, 14, 30, 45, 0, time.UTC), }, }, + { + name: "ok, OpenAPI date-time format is RFC3339", + contentType: MIMEApplicationForm, + data: "openapi_datetime=2023-12-25T14:30:45Z", + expect: TestStruct{ + OpenAPIDateTime: time.Date(2023, 12, 25, 14, 30, 45, 0, time.UTC), + }, + }, + { + name: "ok, OpenAPI date-time-local format is RFC3339", + contentType: MIMEApplicationForm, + data: "openapi_datetime_local=2023-12-25T14:30:45%2B02:00", + expect: TestStruct{ + OpenAPIDateTimeLocal: time.Date(2023, 12, 25, 12, 30, 45, 0, time.UTC), + }, + }, { name: "nok, invalid format should fail", contentType: MIMEApplicationForm, @@ -1695,6 +1713,14 @@ func TestTimeFormatBinding(t *testing.T) { "PtrTime: expected %v, got %v", expectedPtr, *result.PtrTime) } } + if !tc.expect.OpenAPIDateTime.IsZero() { + assert.True(t, tc.expect.OpenAPIDateTime.Equal(result.OpenAPIDateTime), + "OpenAPIDateTime: expected %v, got %v", tc.expect.OpenAPIDateTime, result.OpenAPIDateTime) + } + if !tc.expect.OpenAPIDateTimeLocal.IsZero() { + assert.True(t, tc.expect.OpenAPIDateTimeLocal.Equal(result.OpenAPIDateTimeLocal), + "OpenAPIDateTimeLocal: expected %v, got %v", tc.expect.OpenAPIDateTimeLocal, result.OpenAPIDateTimeLocal) + } }) } }