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
25 changes: 25 additions & 0 deletions fiberoapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,11 @@ func collectAllTypes(t reflect.Type, collected map[string]reflect.Type) {
// Handle pointers
t = dereferenceType(t)

// time.Time is rendered inline as a date-time string, not as a component schema
if isTimeType(t) {
return
}

Comment on lines +378 to +382
typeName := getTypeName(t)
if typeName == "" {
return
Expand Down Expand Up @@ -571,6 +576,11 @@ func getSimpleTypeName(t reflect.Type) string {
}
}

// isTimeType reports whether t is the standard library time.Time type.
func isTimeType(t reflect.Type) bool {
return t != nil && t.Kind() == reflect.Struct && t.Name() == "Time" && t.PkgPath() == "time"
}

// generateSchema generates an OpenAPI schema from a Go type
func generateSchema(t reflect.Type) map[string]interface{} {
if t == nil {
Expand All @@ -582,6 +592,14 @@ func generateSchema(t reflect.Type) map[string]interface{} {
// Handle pointers
t = dereferenceType(t)

if isTimeType(t) {
return map[string]interface{}{
"type": "string",
"format": "date-time",
"example": "2006-01-02T15:04:05Z",
}
}

schema := make(map[string]interface{})

switch t.Kind() {
Expand Down Expand Up @@ -707,6 +725,13 @@ func generateFieldSchema(t reflect.Type) map[string]interface{} {
// Handle pointers
t = dereferenceType(t)

if isTimeType(t) {
schema["type"] = "string"
schema["format"] = "date-time"
schema["example"] = "2006-01-02T15:04:05Z"
return schema
}

switch t.Kind() {
case reflect.String:
schema["type"] = "string"
Expand Down
121 changes: 121 additions & 0 deletions time_type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package fiberoapi

import (
"encoding/json"
"io"
"net/http/httptest"
"testing"
"time"

"github.com/gofiber/fiber/v2"
)

type Workspace struct {
WorkspaceID string `json:"workspace_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

type WorkspaceResponse struct {
Workspaces []Workspace `json:"workspaces"`
}

type EmptyRequest struct{}

func TestTimeTypeRendersAsDateTimeString(t *testing.T) {
app := fiber.New()
oapi := New(app)

Post(oapi, "/workspaces", func(c *fiber.Ctx, req *EmptyRequest) (*WorkspaceResponse, *ErrorResponse) {
return &WorkspaceResponse{}, nil
Comment on lines +25 to +30
}, OpenAPIOptions{
OperationID: "listWorkspaces",
Tags: []string{"workspaces"},
})

oapi.SetupDocs()

req := httptest.NewRequest("GET", "/openapi.json", nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

body, _ := io.ReadAll(resp.Body)
var spec map[string]interface{}
if err := json.Unmarshal(body, &spec); err != nil {
t.Fatalf("Failed to parse OpenAPI JSON: %v", err)
}

components := spec["components"].(map[string]interface{})
schemas := components["schemas"].(map[string]interface{})

if _, exists := schemas["Time"]; exists {
t.Errorf("time.Time should not be registered as a 'Time' component schema")
}

workspaceSchema, ok := schemas["Workspace"].(map[string]interface{})
if !ok {
t.Fatal("Expected Workspace schema to be present")
}
props := workspaceSchema["properties"].(map[string]interface{})

for _, field := range []string{"created_at", "updated_at"} {
f, ok := props[field].(map[string]interface{})
if !ok {
t.Fatalf("Expected Workspace.%s to be an object", field)
}
if f["type"] != "string" {
t.Errorf("Expected %s.type to be 'string', got %v", field, f["type"])
}
if f["format"] != "date-time" {
t.Errorf("Expected %s.format to be 'date-time', got %v", field, f["format"])
}
if _, hasRef := f["$ref"]; hasRef {
t.Errorf("Expected %s to be inlined, not a $ref", field)
}
}
}

type EventWithPointerTime struct {
Name string `json:"name"`
StartedAt *time.Time `json:"started_at,omitempty"`
}

func TestPointerTimeTypeRendersAsDateTimeString(t *testing.T) {
app := fiber.New()
oapi := New(app)

Post(oapi, "/events", func(c *fiber.Ctx, req *EmptyRequest) (*EventWithPointerTime, *ErrorResponse) {
return &EventWithPointerTime{}, nil
}, OpenAPIOptions{
OperationID: "createEvent",
Tags: []string{"events"},
})

oapi.SetupDocs()

req := httptest.NewRequest("GET", "/openapi.json", nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

body, _ := io.ReadAll(resp.Body)
var spec map[string]interface{}
if err := json.Unmarshal(body, &spec); err != nil {
t.Fatalf("Failed to parse OpenAPI JSON: %v", err)
}

schemas := spec["components"].(map[string]interface{})["schemas"].(map[string]interface{})
eventSchema := schemas["EventWithPointerTime"].(map[string]interface{})
props := eventSchema["properties"].(map[string]interface{})

startedAt, ok := props["started_at"].(map[string]interface{})
if !ok {
t.Fatal("Expected started_at property to be present")
}
if startedAt["type"] != "string" || startedAt["format"] != "date-time" {
t.Errorf("Expected *time.Time to render as string/date-time, got %v", startedAt)
}
}
Loading