Repository: deepmap/oapi-codegen
Pull request #92 (2019-11-12 17:27:50)

Added custom types for un/marshaling
This adds special handling for "date" and "byte" string formats in OpenAPI 3.  When marshaled, "date" strings should have their time truncated, and "byte" strings should be Base64 encoded.

This is to address https://github.com/deepmap/oapi-codegen/issues/90.

Needs a bit more testing, but I'd appreciate any comments.

jmcnevin: After some research, it appears that []byte is JSON marshaled as base64 by default, so that custom type may not even be necessary.
jmcnevin: All set.  This backed out some of my other changes.

Patch:
diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go
index 8140a08..0caf4dd 100644
--- a/pkg/codegen/codegen.go
+++ b/pkg/codegen/codegen.go
@@ -39,6 +39,48 @@ type Options struct {
 	SkipFmt            bool // Whether to skip go fmt on the generated code
 }
 
+type goImport struct {
+	lookFor     string
+	alias       string
+	packageName string
+}
+
+func (i goImport) String() string {
+	if i.alias != "" {
+		return fmt.Sprintf("%s %q", i.alias, i.packageName)
+	}
+	return fmt.Sprintf("%q", i.packageName)
+}
+
+type goImports []goImport
+
+var (
+	allGoImports = goImports{
+		{lookFor: "base64.", packageName: "encoding/base64"},
+		{lookFor: "bytes.", packageName: "bytes"},
+		{lookFor: "chi.", packageName: "github.com/go-chi/chi"},
+		{lookFor: "context.", packageName: "context"},
+		{lookFor: "echo.", packageName: "github.com/labstack/echo/v4"},
+		{lookFor: "errors.", packageName: "github.com/pkg/errors"},
+		{lookFor: "fmt.", packageName: "fmt"},
+		{lookFor: "gzip.", packageName: "compress/gzip"},
+		{lookFor: "http.", packageName: "net/http"},
+		{lookFor: "io.", packageName: "io"},
+		{lookFor: "ioutil.", packageName: "io/ioutil"},
+		{lookFor: "json.", packageName: "encoding/json"},
+		{lookFor: "openapi3.", packageName: "github.com/getkin/kin-openapi/openapi3"},
+		{lookFor: "openapi_types.", alias: "openapi_types", packageName: "github.com/deepmap/oapi-codegen/pkg/types"},
+		{lookFor: "path.", packageName: "path"},
+		{lookFor: "runtime.", packageName: "github.com/deepmap/oapi-codegen/pkg/runtime"},
+		{lookFor: "strings.", packageName: "strings"},
+		{lookFor: "time.Duration", packageName: "time"},
+		{lookFor: "time.Time", packageName: "time"},
+		{lookFor: "url.", packageName: "net/url"},
+		{lookFor: "xml.", packageName: "encoding/xml"},
+		{lookFor: "yaml.", packageName: "gopkg.in/yaml.v2"},
+	}
+)
+
 // Uses the Go templating engine to generate all of our server wrappers from
 // the descriptions we've built up above from the schema objects.
 // opts defines
@@ -114,71 +156,10 @@ func Generate(swagger *openapi3.Swagger, packageName string, opts Options) (stri
 	// Based on module prefixes, figure out which optional imports are required.
 	// TODO: this is error prone, use tighter matches
 	for _, str := range []string{typeDefinitions, chiServerOut, echoServerOut, clientOut, clientWithResponsesOut, inlinedSpec} {
-		if strings.Contains(str, "time.Duration") {
-			imports = append(imports, "time")
-		}
-		if strings.Contains(str, "time.Time") {
-			imports = append(imports, "time")
-		}
-		if strings.Contains(str, "http.") {
-			imports = append(imports, "net/http")
-		}
-		if strings.Contains(str, "openapi3.") {
-			imports = append(imports, "github.com/getkin/kin-openapi/openapi3")
-		}
-		if strings.Contains(str, "json.") {
-			imports = append(imports, "encoding/json")
-		}
-		if strings.Contains(str, "echo.") {
-			imports = append(imports, "github.com/labstack/echo/v4")
-		}
-		if strings.Contains(str, "io.") {
-			imports = append(imports, "io")
-		}
-		if strings.Contains(str, "ioutil.") {
-			imports = append(imports, "io/ioutil")
-		}
-		if strings.Contains(str, "url.") {
-			imports = append(imports, "net/url")
-		}
-		if strings.Contains(str, "path.") {
-			imports = append(imports, "path")
-		}
-		if strings.Contains(str, "context.") {
-			imports = append(imports, "context")
-		}
-		if strings.Contains(str, "runtime.") {
-			imports = append(imports, "github.com/deepmap/oapi-codegen/pkg/runtime")
-		}
-		if strings.Contains(str, "bytes.") {
-			imports = append(imports, "bytes")
-		}
-		if strings.Contains(str, "gzip.") {
-			imports = append(imports, "compress/gzip")
-		}
-		if strings.Contains(str, "base64.") {
-			imports = append(imports, "encoding/base64")
-		}
-		if strings.Contains(str, "openapi3.") {
-			imports = append(imports, "github.com/getkin/kin-openapi/openapi3")
-		}
-		if strings.Contains(str, "strings.") {
-			imports = append(imports, "strings")
-		}
-		if strings.Contains(str, "fmt.") {
-			imports = append(imports, "fmt")
-		}
-		if strings.Contains(str, "yaml.") {
-			imports = append(imports, "gopkg.in/yaml.v2")
-		}
-		if strings.Contains(str, "xml.") {
-			imports = append(imports, "encoding/xml")
-		}
-		if strings.Contains(str, "errors.") {
-			imports = append(imports, "github.com/pkg/errors")
-		}
-		if strings.Contains(str, "chi.") {
-			imports = append(imports, "github.com/go-chi/chi")
+		for _, goImport := range allGoImports {
+			if strings.Contains(str, goImport.lookFor) {
+				imports = append(imports, goImport.String())
+			}
 		}
 	}
 
diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go
index 316e098..6db35e8 100644
--- a/pkg/codegen/schema.go
+++ b/pkg/codegen/schema.go
@@ -246,7 +246,11 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) {
 		case "string":
 			// Special case string formats here.
 			switch f {
-			case "date-time", "date":
+			case "byte":
+				outSchema.GoType = "[]byte"
+			case "date":
+				outSchema.GoType = "openapi_types.Date"
+			case "date-time":
 				outSchema.GoType = "time.Time"
 			case "json":
 				outSchema.GoType = "json.RawMessage"
diff --git a/pkg/codegen/templates/imports.tmpl b/pkg/codegen/templates/imports.tmpl
index 001318f..b5c5f1e 100644
--- a/pkg/codegen/templates/imports.tmpl
+++ b/pkg/codegen/templates/imports.tmpl
@@ -5,6 +5,6 @@ package {{.PackageName}}
 
 {{if .Imports}}
 import (
-{{range .Imports}} "{{.}}"
+{{range .Imports}} {{ . }}
 {{end}})
 {{end}}
diff --git a/pkg/codegen/templates/templates.gen.go b/pkg/codegen/templates/templates.gen.go
index 64aed73..6f562ef 100644
--- a/pkg/codegen/templates/templates.gen.go
+++ b/pkg/codegen/templates/templates.gen.go
@@ -653,7 +653,7 @@ package {{.PackageName}}
 
 {{if .Imports}}
 import (
-{{range .Imports}} "{{.}}"
+{{range .Imports}} {{ . }}
 {{end}})
 {{end}}
 `,
diff --git a/pkg/types/date.go b/pkg/types/date.go
new file mode 100644
index 0000000..5d981a2
--- /dev/null
+++ b/pkg/types/date.go
@@ -0,0 +1,30 @@
+package types
+
+import (
+	"encoding/json"
+	"time"
+)
+
+const dateFormat = "2006-01-02"
+
+type Date struct {
+	time.Time
+}
+
+func (d Date) MarshalJSON() ([]byte, error) {
+	return json.Marshal(d.Time.Format(dateFormat))
+}
+
+func (d *Date) UnmarshalJSON(data []byte) error {
+	var dateStr string
+	err := json.Unmarshal(data, &dateStr)
+	if err != nil {
+		return err
+	}
+	parsed, err := time.Parse(dateFormat, dateStr)
+	if err != nil {
+		return err
+	}
+	d.Time = parsed
+	return nil
+}


Test patch:
diff --git a/internal/test/server/server.gen.go b/internal/test/server/server.gen.go
index ee6e480..5f39d1e 100644
--- a/internal/test/server/server.gen.go
+++ b/internal/test/server/server.gen.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"fmt"
 	"github.com/deepmap/oapi-codegen/pkg/runtime"
+	openapi_types "github.com/deepmap/oapi-codegen/pkg/types"
 	"github.com/go-chi/chi"
 	"net/http"
 	"time"
@@ -14,13 +15,14 @@ import (
 
 // EveryTypeOptional defines model for EveryTypeOptional.
 type EveryTypeOptional struct {
-	ArrayInlineField     *[]int        `json:"array_inline_field,omitempty"`
-	ArrayReferencedField *[]SomeObject `json:"array_referenced_field,omitempty"`
-	BoolField            *bool         `json:"bool_field,omitempty"`
-	DateField            *time.Time    `json:"date_field,omitempty"`
-	DateTimeField        *time.Time    `json:"date_time_field,omitempty"`
-	DoubleField          *float64      `json:"double_field,omitempty"`
-	FloatField           *float32      `json:"float_field,omitempty"`
+	ArrayInlineField     *[]int              `json:"array_inline_field,omitempty"`
+	ArrayReferencedField *[]SomeObject       `json:"array_referenced_field,omitempty"`
+	BoolField            *bool               `json:"bool_field,omitempty"`
+	ByteField            *[]byte             `json:"byte_field,omitempty"`
+	DateField            *openapi_types.Date `json:"date_field,omitempty"`
+	DateTimeField        *time.Time          `json:"date_time_field,omitempty"`
+	DoubleField          *float64            `json:"double_field,omitempty"`
+	FloatField           *float32            `json:"float_field,omitempty"`
 	InlineObjectField    *struct {
 		Name   string `json:"name"`
 		Number int    `json:"number"`
@@ -35,13 +37,14 @@ type EveryTypeOptional struct {
 
 // EveryTypeRequired defines model for EveryTypeRequired.
 type EveryTypeRequired struct {
-	ArrayInlineField     []int        `json:"array_inline_field"`
-	ArrayReferencedField []SomeObject `json:"array_referenced_field"`
-	BoolField            bool         `json:"bool_field"`
-	DateField            time.Time    `json:"date_field"`
-	DateTimeField        time.Time    `json:"date_time_field"`
-	DoubleField          float64      `json:"double_field"`
-	FloatField           float32      `json:"float_field"`
+	ArrayInlineField     []int              `json:"array_inline_field"`
+	ArrayReferencedField []SomeObject       `json:"array_referenced_field"`
+	BoolField            bool               `json:"bool_field"`
+	ByteField            []byte             `json:"byte_field"`
+	DateField            openapi_types.Date `json:"date_field"`
+	DateTimeField        time.Time          `json:"date_time_field"`
+	DoubleField          float64            `json:"double_field"`
+	FloatField           float32            `json:"float_field"`
 	InlineObjectField    struct {
 		Name   string `json:"name"`
 		Number int    `json:"number"`
diff --git a/internal/test/test-schema.yaml b/internal/test/test-schema.yaml
index 22365f2..8bf3702 100644
--- a/internal/test/test-schema.yaml
+++ b/internal/test/test-schema.yaml
@@ -218,6 +218,9 @@ components:
         date_field:
           type: string
           format: date
+        byte_field:
+          type: string
+          format: byte
         date_time_field:
           type: string
           format: date-time
@@ -255,6 +258,7 @@ components:
         - referenced_field
         - array_inline_field
         - array_referenced_field
+        - byte_field
     EveryTypeOptional:
       properties:
         int32_field:
@@ -280,6 +284,9 @@ components:
         date_field:
           type: string
           format: date
+        byte_field:
+          type: string
+          format: byte
         date_time_field:
           type: string
           format: date-time
diff --git a/pkg/types/date_test.go b/pkg/types/date_test.go
new file mode 100644
index 0000000..1369b1c
--- /dev/null
+++ b/pkg/types/date_test.go
@@ -0,0 +1,32 @@
+package types
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDate_MarshalJSON(t *testing.T) {
+	testDate := time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC)
+	b := struct {
+		DateField Date	`json:"date"`
+	}{
+		DateField: Date{testDate},
+	}
+	jsonBytes, err := json.Marshal(b)
+	assert.NoError(t, err)
+	assert.JSONEq(t, `{"date":"2019-04-01"}`, string(jsonBytes))
+}
+
+func TestDate_UnmarshalJSON(t *testing.T) {
+	testDate := time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC)
+	jsonStr := `{"date":"2019-04-01"}`
+	b := struct {
+		DateField Date	`json:"date"`
+	}{}
+	err := json.Unmarshal([]byte(jsonStr), &b)
+	assert.NoError(t, err)
+	assert.Equal(t, testDate, b.DateField.Time)
+}