diff --git a/README.md b/README.md index 9a4d1e9..ee6e98e 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,29 @@ api.Walk(func(path string, endpoint *swagger.Endpoint) { }) ``` +### Custom Types + +For types implementing `json.Marshaler` whose JSON output does not match their Go types (such as `time.Time`), +it is possible to override the default scanning of types and define a property manually. + +For example: + +```go +type Person struct { + Name string `json:"name"` + Birthday time.Time `json:"birthday"` +} + +func main() { + RegisterCustomType(time.Time{}, Property{ + Type: "string", + Format: "date-time", + }) +} +``` + +`time.Time` is automatically registered as a custom type. + ## Complete Example ```go diff --git a/examples/builtin/main.go b/examples/builtin/main.go index c762704..0403db4 100644 --- a/examples/builtin/main.go +++ b/examples/builtin/main.go @@ -3,6 +3,7 @@ package main import ( "io" "net/http" + "time" "github.com/savaki/swag" "github.com/savaki/swag/endpoint" @@ -20,11 +21,13 @@ type Category struct { // Pet example from the swagger pet store type Pet struct { - ID int64 `json:"id"` - Category Category `json:"category"` - Name string `json:"name"` - PhotoUrls []string `json:"photoUrls"` - Tags []string `json:"tags"` + ID int64 `json:"id"` + Category Category `json:"category"` + Name string `json:"name"` + PhotoUrls []string `json:"photoUrls"` + Tags []string `json:"tags"` + CreatedAt time.Time `json:"createdAt"` + DeletedAt *time.Time `json:"deletedAt"` } func main() { diff --git a/swagger/reflect.go b/swagger/reflect.go index aeffec1..1cf74e3 100644 --- a/swagger/reflect.go +++ b/swagger/reflect.go @@ -3,9 +3,52 @@ package swagger import ( "reflect" "strings" + "time" ) +var customTypes map[reflect.Type]Property + +func init() { + customTypes = map[reflect.Type]Property{} + + RegisterCustomType(time.Time{}, Property{ + Type: "string", + Format: "date-time", + }) +} + +// RegisterCustomType maps a reflect.Type to a pre-defined Property. This can be +// used to handle types that implement json.Marshaler or other interfaces. +// For example, a property with a Go type of time.Time would be represented as +// an object when it should be a string. +// +// RegisterCustomType(time.Time{}, Property{ +// Type: "string", +// Format: "date-time", +// }) +// +// Pointers to registered types will resolve to the same Property value unless +// that pointer type has also been registered as a custom type. +// +// For example: registering time.Time will also apply to *time.Time, unless +// *time.Time has also been registered. +func RegisterCustomType(v interface{}, p Property) { + t := reflect.TypeOf(v) + p.GoType = t + customTypes[t] = p +} + func inspect(t reflect.Type, jsonTag string) Property { + if p, ok := customTypes[t]; ok { + return p + } + + if t.Kind() == reflect.Ptr { + if p, ok := customTypes[t.Elem()]; ok { + return p + } + } + p := Property{ GoType: t, } @@ -174,6 +217,9 @@ func define(v interface{}) map[string]Object { dirty = false for _, d := range objMap { for _, p := range d.Properties { + if _, ok := customTypes[p.GoType]; ok { + continue + } if p.GoType.Kind() == reflect.Struct { name := makeName(p.GoType) if _, exists := objMap[name]; !exists { diff --git a/swagger/reflect_test.go b/swagger/reflect_test.go index 125dd66..bd9f43d 100644 --- a/swagger/reflect_test.go +++ b/swagger/reflect_test.go @@ -3,11 +3,12 @@ package swagger import ( "bytes" "encoding/json" + "fmt" "io/ioutil" "testing" + "time" "github.com/stretchr/testify/assert" - "fmt" ) type Person struct { @@ -23,6 +24,8 @@ type Pet struct { IntArray []int String string StringArray []string + Time time.Time + TimePtr *time.Time } type Empty struct { @@ -40,7 +43,7 @@ func TestDefine(t *testing.T) { obj, ok := v["swaggerPet"] assert.True(t, ok) assert.False(t, obj.IsArray) - assert.Equal(t, 8, len(obj.Properties)) + assert.Equal(t, 10, len(obj.Properties)) content := map[string]Object{} data, err := ioutil.ReadFile("testdata/pet.json") @@ -114,3 +117,14 @@ func TestHonorJsonIgnore(t *testing.T) { assert.False(t, obj.IsArray) assert.Equal(t, 0, len(obj.Properties), "expected zero exposed properties") } + +func TestCustomTypes(t *testing.T) { + type ContainsCustomType struct { + TestTime time.Time `json:"testTime"` + } + + obj := defineObject(ContainsCustomType{}) + + assert.Contains(t, obj.Properties, "testTime") + assert.EqualValues(t, "string", obj.Properties["testTime"].Type) +} diff --git a/swagger/testdata/pet.json b/swagger/testdata/pet.json index c272e74..0ec8348 100644 --- a/swagger/testdata/pet.json +++ b/swagger/testdata/pet.json @@ -33,6 +33,14 @@ "type": "string" } }, + "Time": { + "type": "string", + "format": "date-time" + }, + "TimePtr": { + "type": "string", + "format": "date-time" + }, "friend": { "$ref": "#/definitions/swaggerPerson" }, @@ -53,4 +61,4 @@ } } } -} \ No newline at end of file +}