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
14 changes: 13 additions & 1 deletion bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
}
Expand Down
36 changes: 31 additions & 5 deletions bind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
})
}
}