Repository: deepmap/oapi-codegen
Pull request #117 (2020-01-07 18:02:38)

Allow filtering by tags
I am working on an api that we want implemented by multiple services. The spec has tags like "auth" and "bot" that correspond to the service that will serve the endpoint.  It is handy to be able to generate a server for just the endpoints that have one of these tags.

This PR adds two fields to Options, `IncludeTags` and `ExcludeTags`.  When running `Generate`, the operations in `swagger` will be filtered based on the tags listed in those options.  When `IncludeTags` is non-empty, all operations that aren't tagged with at least one of the listed tags will be removed.  When `ExcludeTags` is non-empty, all operations that are tagged with at least one of the listed tags will be removed.

This has the side effect of modifying the `swagger` pointer being passed to `Generate`.  I don't think that has any practical consequence. If it does, I can find a way to do a deep copy of swagger before filtering.

The new options are exposed to the cli using the `-include-tags` and `-exclude-tags` arguments.


Patch:
diff --git a/README.md b/README.md
index 65351d1..a260248 100644
--- a/README.md
+++ b/README.md
@@ -400,8 +400,16 @@ you can specify any combination of those.
  the generated file in case the spec contains weird strings.
 
 So, for example, if you would like to produce only the server code, you could
-run `oapi-generate --generate types,server`. You could generate `types` and `server`
-into separate files, but both are required for the server code.  
+run `oapi-generate -generate types,server`. You could generate `types` and
+`server` into separate files, but both are required for the server code.
+
+`oapi-codegen` can filter paths base on their tags in the openapi definition.
+Use either `-include-tags` or `-exclude-tags` followed by a comma-separated list
+of tags. For instance, to generate a server that serves all paths except those
+tagged with `auth` or `admin`, use the argument, `-exclude-tags="auth,admin"`.
+To generate a server that only handles `admin` paths, use the argument
+`-include-tags="admin"`. When neither of these arguments is present, all paths
+are generated.
 
 ## What's missing or incomplete
 
diff --git a/cmd/oapi-codegen/oapi-codegen.go b/cmd/oapi-codegen/oapi-codegen.go
index 330f348..1a1b080 100644
--- a/cmd/oapi-codegen/oapi-codegen.go
+++ b/cmd/oapi-codegen/oapi-codegen.go
@@ -35,11 +35,15 @@ func main() {
 		packageName string
 		generate    string
 		outputFile  string
+		includeTags string
+		excludeTags string
 	)
 	flag.StringVar(&packageName, "package", "", "The package name for generated code")
 	flag.StringVar(&generate, "generate", "types,client,server,spec",
 		`Comma-separated list of code to generate; valid options: "types", "client", "chi-server", "server", "skip-fmt", "spec"`)
 	flag.StringVar(&outputFile, "o", "", "Where to output generated code, stdout is default")
+	flag.StringVar(&includeTags, "include-tags", "", "Only include operations with the given tags. Comma-separated list of tags.")
+	flag.StringVar(&excludeTags, "exclude-tags", "", "Exclude operations that are tagged with the given tags. Comma-separated list of tags.")
 	flag.Parse()
 
 	if flag.NArg() < 1 {
@@ -58,7 +62,7 @@ func main() {
 	}
 
 	opts := codegen.Options{}
-	for _, g := range strings.Split(generate, ",") {
+	for _, g := range splitCSVArg(generate) {
 		switch g {
 		case "client":
 			opts.GenerateClient = true
@@ -79,6 +83,9 @@ func main() {
 		}
 	}
 
+	opts.IncludeTags = splitCSVArg(includeTags)
+	opts.ExcludeTags = splitCSVArg(excludeTags)
+
 	if opts.GenerateEchoServer && opts.GenerateChiServer {
 		errExit("can not specify both server and chi-server targets simultaneously")
 	}
@@ -102,3 +109,19 @@ func main() {
 		fmt.Println(code)
 	}
 }
+
+func splitCSVArg(input string) []string {
+	input = strings.TrimSpace(input)
+	if len(input) == 0 {
+		return nil
+	}
+	splitInput := strings.Split(input, ",")
+	args := make([]string, 0, len(splitInput))
+	for _, s := range splitInput {
+		s = strings.TrimSpace(s)
+		if len(s) > 0 {
+			args = append(args, s)
+		}
+	}
+	return args
+}
diff --git a/examples/petstore-expanded/chi/api/petstore.gen.go b/examples/petstore-expanded/chi/api/petstore.gen.go
index 1a3a817..b757bfa 100644
--- a/examples/petstore-expanded/chi/api/petstore.gen.go
+++ b/examples/petstore-expanded/chi/api/petstore.gen.go
@@ -161,8 +161,11 @@ func FindPetByIdCtx(next http.Handler) http.Handler {
 
 // Handler creates http.Handler with routing matching OpenAPI spec.
 func Handler(si ServerInterface) http.Handler {
-	r := chi.NewRouter()
+	return HandlerFromMux(si, chi.NewRouter())
+}
 
+// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
+func HandlerFromMux(si ServerInterface, r *chi.Mux) http.Handler {
 	r.Group(func(r chi.Router) {
 		r.Use(FindPetsCtx)
 		r.Get("/pets", si.FindPets)
diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go
index 0caf4dd..90015ee 100644
--- a/pkg/codegen/codegen.go
+++ b/pkg/codegen/codegen.go
@@ -19,6 +19,7 @@ import (
 	"bytes"
 	"fmt"
 	"go/format"
+	"regexp"
 	"sort"
 	"strings"
 	"text/template"
@@ -31,12 +32,14 @@ import (
 
 // Options defines the optional code to generate.
 type Options struct {
-	GenerateChiServer  bool // GenerateChiServer specifies whether to generate chi server boilerplate
-	GenerateEchoServer bool // GenerateEchoServer specifies whether to generate echo server boilerplate
-	GenerateClient     bool // GenerateClient specifies whether to generate client boilerplate
-	GenerateTypes      bool // GenerateTypes specifies whether to generate type definitions
-	EmbedSpec          bool // Whether to embed the swagger spec in the generated code
-	SkipFmt            bool // Whether to skip go fmt on the generated code
+	GenerateChiServer  bool     // GenerateChiServer specifies whether to generate chi server boilerplate
+	GenerateEchoServer bool     // GenerateEchoServer specifies whether to generate echo server boilerplate
+	GenerateClient     bool     // GenerateClient specifies whether to generate client boilerplate
+	GenerateTypes      bool     // GenerateTypes specifies whether to generate type definitions
+	EmbedSpec          bool     // Whether to embed the swagger spec in the generated code
+	SkipFmt            bool     // Whether to skip go fmt on the generated code
+	IncludeTags        []string // Only include operations that have one of these tags. Ignored when empty.
+	ExcludeTags        []string // Exclude operations that have one of these tags. Ignored when empty.
 }
 
 type goImport struct {
@@ -56,28 +59,28 @@ 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"},
+		{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"},
 	}
 )
 
@@ -85,6 +88,8 @@ var (
 // the descriptions we've built up above from the schema objects.
 // opts defines
 func Generate(swagger *openapi3.Swagger, packageName string, opts Options) (string, error) {
+	filterOperationsByTag(swagger, opts)
+
 	// This creates the golang templates text package
 	t := template.New("oapi-codegen").Funcs(TemplateFunctions)
 	// This parses all of our own template files into the template object
@@ -154,10 +159,13 @@ func Generate(swagger *openapi3.Swagger, packageName string, opts Options) (stri
 	w := bufio.NewWriter(&buf)
 
 	// 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} {
 		for _, goImport := range allGoImports {
-			if strings.Contains(str, goImport.lookFor) {
+			match, err := regexp.MatchString(fmt.Sprintf("[^a-zA-Z0-9_]%s", goImport.lookFor), str)
+			if err != nil {
+				return "", errors.Wrap(err, "error figuring out imports")
+			}
+			if match {
 				imports = append(imports, goImport.String())
 			}
 		}
@@ -492,3 +500,46 @@ func SanitizeCode(goCode string) string {
 	// See: https://groups.google.com/forum/#!topic/golang-nuts/OToNIPdfkks
 	return strings.Replace(goCode, "\uFEFF", "", -1)
 }
+
+func filterOperationsByTag(swagger *openapi3.Swagger, opts Options) {
+	if len(opts.ExcludeTags) > 0 {
+		excludeOperationsWithTags(swagger.Paths, opts.ExcludeTags)
+	}
+	if len(opts.IncludeTags) > 0 {
+		includeOperationsWithTags(swagger.Paths, opts.IncludeTags, false)
+	}
+}
+
+func excludeOperationsWithTags(paths openapi3.Paths, tags []string) {
+	includeOperationsWithTags(paths, tags, true)
+}
+
+func includeOperationsWithTags(paths openapi3.Paths, tags []string, exclude bool) {
+	for _, pathItem := range paths {
+		ops := pathItem.Operations()
+		names := make([]string, 0, len(ops))
+		for name, op := range ops {
+			if operationHasTag(op, tags) == exclude {
+				names = append(names, name)
+			}
+		}
+		for _, name := range names {
+			pathItem.SetOperation(name, nil)
+		}
+	}
+}
+
+//operationHasTag returns true if the operation is tagged with any of tags
+func operationHasTag(op *openapi3.Operation, tags []string) bool {
+	if op == nil {
+		return false
+	}
+	for _, hasTag := range op.Tags {
+		for _, wantTag := range tags {
+			if hasTag == wantTag {
+				return true
+			}
+		}
+	}
+	return false
+}
diff --git a/pkg/codegen/templates/chi-handler.tmpl b/pkg/codegen/templates/chi-handler.tmpl
index 335a1e6..f6bfad2 100644
--- a/pkg/codegen/templates/chi-handler.tmpl
+++ b/pkg/codegen/templates/chi-handler.tmpl
@@ -1,7 +1,10 @@
 // Handler creates http.Handler with routing matching OpenAPI spec.
 func Handler(si ServerInterface) http.Handler {
-  r := chi.NewRouter()
+  return HandlerFromMux(si, chi.NewRouter())
+}
 
+// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
+func HandlerFromMux(si ServerInterface, r *chi.Mux) http.Handler {
 {{range .}}r.Group(func(r chi.Router) {
   r.Use({{.OperationId}}Ctx)
   r.{{.Method | lower | title }}("{{.Path | swaggerUriToChiUri}}", si.{{.OperationId}})
diff --git a/pkg/codegen/templates/templates.gen.go b/pkg/codegen/templates/templates.gen.go
index fedb94f..0c7f6a6 100644
--- a/pkg/codegen/templates/templates.gen.go
+++ b/pkg/codegen/templates/templates.gen.go
@@ -75,8 +75,11 @@ func (a {{.TypeName}}) MarshalJSON() ([]byte, error) {
 `,
 	"chi-handler.tmpl": `// Handler creates http.Handler with routing matching OpenAPI spec.
 func Handler(si ServerInterface) http.Handler {
-  r := chi.NewRouter()
+  return HandlerFromMux(si, chi.NewRouter())
+}
 
+// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
+func HandlerFromMux(si ServerInterface, r *chi.Mux) http.Handler {
 {{range .}}r.Group(func(r chi.Router) {
   r.Use({{.OperationId}}Ctx)
   r.{{.Method | lower | title }}("{{.Path | swaggerUriToChiUri}}", si.{{.OperationId}})
diff --git a/pkg/runtime/bindparam.go b/pkg/runtime/bindparam.go
index f4b1c31..18a4130 100644
--- a/pkg/runtime/bindparam.go
+++ b/pkg/runtime/bindparam.go
@@ -21,6 +21,7 @@ import (
 	"reflect"
 	"strings"
 
+	"github.com/deepmap/oapi-codegen/pkg/types"
 	"github.com/labstack/echo/v4"
 )
 
@@ -299,6 +300,7 @@ func BindQueryParameter(style string, explode bool, required bool, paramName str
 			// different things based on destination type.
 			values, found := queryParams[paramName]
 			var err error
+
 			switch k {
 			case reflect.Slice:
 				// In the slice case, we simply use the arguments provided by
@@ -430,6 +432,12 @@ func BindQueryParameter(style string, explode bool, required bool, paramName str
 // We don't try to be smart here, if the field exists as a query argument,
 // set its value.
 func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) error {
+	// special handling for custom types
+	switch dest.(type) {
+	case *types.Date:
+		return BindStringToObject(values.Get(paramName), dest)
+	}
+
 	v := reflect.Indirect(reflect.ValueOf(dest))
 	if v.Type().Kind() != reflect.Struct {
 		return echo.NewHTTPError(http.StatusInternalServerError,
diff --git a/pkg/runtime/bindstring.go b/pkg/runtime/bindstring.go
index 384427d..f3397a2 100644
--- a/pkg/runtime/bindstring.go
+++ b/pkg/runtime/bindstring.go
@@ -19,6 +19,8 @@ import (
 	"reflect"
 	"strconv"
 	"time"
+
+	"github.com/deepmap/oapi-codegen/pkg/types"
 )
 
 // This function takes a string, and attempts to assign it to the destination
@@ -68,13 +70,21 @@ func BindStringToObject(src string, dst interface{}) error {
 			v.SetBool(val)
 		}
 	case reflect.Struct:
-		// Time is a special case of a struct that we handle
-		if dstTime, ok := dst.(*time.Time); ok {
+		switch dstType := dst.(type) {
+		case *time.Time:
+			// Time is a special case of a struct that we handle
 			parsedTime, err := time.Parse(time.RFC3339Nano, src)
 			if err != nil {
 				return fmt.Errorf("error parsing '%s' as RFC3339 time: %s", src, err)
 			}
-			*dstTime = parsedTime
+			*dstType = parsedTime
+			return nil
+		case *types.Date:
+			parsedTime, err := time.Parse(types.DateFormat, src)
+			if err != nil {
+				return fmt.Errorf("error parsing '%s' as date: %s", src, err)
+			}
+			dstType.Time = parsedTime
 			return nil
 		}
 		fallthrough
diff --git a/pkg/runtime/styleparam.go b/pkg/runtime/styleparam.go
index f6439e8..3edcce6 100644
--- a/pkg/runtime/styleparam.go
+++ b/pkg/runtime/styleparam.go
@@ -14,6 +14,7 @@
 package runtime
 
 import (
+	"errors"
 	"fmt"
 	"reflect"
 	"sort"
@@ -48,6 +49,8 @@ func StyleParam(style string, explode bool, paramName string, value interface{})
 		return styleSlice(style, explode, paramName, sliceVal)
 	case reflect.Struct:
 		return styleStruct(style, explode, paramName, value)
+	case reflect.Map:
+		return styleMap(style, explode, paramName, value)
 	default:
 		return stylePrimitive(style, explode, paramName, value)
 	}
@@ -161,6 +164,28 @@ func styleStruct(style string, explode bool, paramName string, value interface{}
 		fieldDict[fieldName] = str
 	}
 
+	return processFieldDict(style, explode, paramName, fieldDict)
+}
+
+func styleMap(style string, explode bool, paramName string, value interface{}) (string, error) {
+	dict, ok := value.(map[string]interface{})
+	if !ok {
+		return "", errors.New("map not of type map[string]interface{}")
+	}
+
+	fieldDict := make(map[string]string)
+	for fieldName, value := range dict {
+		str, err := primitiveToString(value)
+		if err != nil {
+			return "", fmt.Errorf("error formatting '%s': %s", paramName, err)
+		}
+		fieldDict[fieldName] = str
+	}
+
+	return processFieldDict(style, explode, paramName, fieldDict)
+}
+
+func processFieldDict(style string, explode bool, paramName string, fieldDict map[string]string) (string, error) {
 	var parts []string
 
 	// This works for everything except deepObject. We'll handle that one
diff --git a/pkg/types/date.go b/pkg/types/date.go
index 5d981a2..bdf94a9 100644
--- a/pkg/types/date.go
+++ b/pkg/types/date.go
@@ -5,14 +5,14 @@ import (
 	"time"
 )
 
-const dateFormat = "2006-01-02"
+const DateFormat = "2006-01-02"
 
 type Date struct {
 	time.Time
 }
 
 func (d Date) MarshalJSON() ([]byte, error) {
-	return json.Marshal(d.Time.Format(dateFormat))
+	return json.Marshal(d.Time.Format(DateFormat))
 }
 
 func (d *Date) UnmarshalJSON(data []byte) error {
@@ -21,7 +21,7 @@ func (d *Date) UnmarshalJSON(data []byte) error {
 	if err != nil {
 		return err
 	}
-	parsed, err := time.Parse(dateFormat, dateStr)
+	parsed, err := time.Parse(DateFormat, dateStr)
 	if err != nil {
 		return err
 	}


Test patch:
diff --git a/internal/test/server/server.gen.go b/internal/test/server/server.gen.go
index c30b55f..32f5d35 100644
--- a/internal/test/server/server.gen.go
+++ b/internal/test/server/server.gen.go
@@ -361,8 +361,11 @@ func UpdateResource3Ctx(next http.Handler) http.Handler {
 
 // Handler creates http.Handler with routing matching OpenAPI spec.
 func Handler(si ServerInterface) http.Handler {
-	r := chi.NewRouter()
+	return HandlerFromMux(si, chi.NewRouter())
+}
 
+// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
+func HandlerFromMux(si ServerInterface, r *chi.Mux) http.Handler {
 	r.Group(func(r chi.Router) {
 		r.Use(GetSimpleCtx)
 		r.Get("/get-simple", si.GetSimple)
diff --git a/pkg/codegen/codegen_test.go b/pkg/codegen/codegen_test.go
index 9ca8db7..13c1150 100644
--- a/pkg/codegen/codegen_test.go
+++ b/pkg/codegen/codegen_test.go
@@ -9,7 +9,6 @@ import (
 
 	examplePetstoreClient "github.com/deepmap/oapi-codegen/examples/petstore-expanded"
 	examplePetstore "github.com/deepmap/oapi-codegen/examples/petstore-expanded/echo/api"
-
 	"github.com/getkin/kin-openapi/openapi3"
 	"github.com/golangci/lint-1"
 	"github.com/stretchr/testify/assert"
@@ -72,6 +71,51 @@ func TestExamplePetStoreParseFunction(t *testing.T) {
 	assert.Equal(t, "cat", *findPetByIDResponse.JSON200.Tag)
 }
 
+func TestFilterOperationsByTag(t *testing.T) {
+	packageName := "testswagger"
+	t.Run("include tags", func(t *testing.T) {
+		opts := Options{
+			GenerateClient:     true,
+			GenerateEchoServer: true,
+			GenerateTypes:      true,
+			EmbedSpec:          true,
+			IncludeTags:        []string{"hippo", "giraffe", "cat"},
+		}
+
+		// Get a spec from the test definition in this file:
+		swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData([]byte(testOpenAPIDefinition))
+		assert.NoError(t, err)
+
+		// Run our code generation:
+		code, err := Generate(swagger, packageName, opts)
+		assert.NoError(t, err)
+		assert.NotEmpty(t, code)
+		assert.NotContains(t, code, `"/test/:name"`)
+		assert.Contains(t, code, `"/cat"`)
+	})
+
+	t.Run("exclude tags", func(t *testing.T) {
+		opts := Options{
+			GenerateClient:     true,
+			GenerateEchoServer: true,
+			GenerateTypes:      true,
+			EmbedSpec:          true,
+			ExcludeTags:        []string{"hippo", "giraffe", "cat"},
+		}
+
+		// Get a spec from the test definition in this file:
+		swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData([]byte(testOpenAPIDefinition))
+		assert.NoError(t, err)
+
+		// Run our code generation:
+		code, err := Generate(swagger, packageName, opts)
+		assert.NoError(t, err)
+		assert.NotEmpty(t, code)
+		assert.Contains(t, code, `"/test/:name"`)
+		assert.NotContains(t, code, `"/cat"`)
+	})
+}
+
 func TestExampleOpenAPICodeGeneration(t *testing.T) {
 
 	// Input vars for code generation:
diff --git a/pkg/runtime/bindparam_test.go b/pkg/runtime/bindparam_test.go
index c475183..fcb8275 100644
--- a/pkg/runtime/bindparam_test.go
+++ b/pkg/runtime/bindparam_test.go
@@ -16,7 +16,9 @@ package runtime
 import (
 	"net/url"
 	"testing"
+	"time"
 
+	"github.com/deepmap/oapi-codegen/pkg/types"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -229,24 +231,43 @@ func TestSplitParameter(t *testing.T) {
 }
 
 func TestBindQueryParameter(t *testing.T) {
-	type ID struct {
-		FirstName *string `json:"firstName"`
-		LastName  *string `json:"lastName"`
-		Role      string  `json:"role"`
-	}
-
-	expectedName := "Alex"
-	expectedDeepObject := &ID{FirstName: &expectedName, Role: "admin"}
-
-	actual := new(ID)
-	paramName := "id"
-	queryParams := url.Values{
-		"id[firstName]": {"Alex"},
-		"id[role]":      {"admin"},
-		"foo":           {"bar"},
-	}
-
-	err := BindQueryParameter("deepObject", true, false, paramName, queryParams, &actual)
-	assert.NoError(t, err)
-	assert.Equal(t, expectedDeepObject, actual)
+	t.Run("deepObject", func(t *testing.T) {
+		type ID struct {
+			FirstName *string     `json:"firstName"`
+			LastName  *string     `json:"lastName"`
+			Role      string      `json:"role"`
+			Birthday  *types.Date `json:"birthday"`
+		}
+
+		expectedName := "Alex"
+		expectedDeepObject := &ID{
+			FirstName: &expectedName,
+			Role:      "admin",
+			Birthday:  &types.Date{time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)},
+		}
+
+		actual := new(ID)
+		paramName := "id"
+		queryParams := url.Values{
+			"id[firstName]": {"Alex"},
+			"id[role]":      {"admin"},
+			"foo":           {"bar"},
+			"id[birthday]":  {"2020-01-01"},
+		}
+
+		err := BindQueryParameter("deepObject", true, false, paramName, queryParams, &actual)
+		assert.NoError(t, err)
+		assert.Equal(t, expectedDeepObject, actual)
+	})
+
+	t.Run("form", func(t *testing.T) {
+		expected := &types.Date{time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}
+		birthday := &types.Date{}
+		queryParams := url.Values{
+			"birthday": {"2020-01-01"},
+		}
+		err := BindQueryParameter("form", true, false, "birthday", queryParams, &birthday)
+		assert.NoError(t, err)
+		assert.Equal(t, expected, birthday)
+	})
 }
diff --git a/pkg/runtime/styleparam_test.go b/pkg/runtime/styleparam_test.go
index e7f4b7f..d908b59 100644
--- a/pkg/runtime/styleparam_test.go
+++ b/pkg/runtime/styleparam_test.go
@@ -30,6 +30,9 @@ func TestStyleParam(t *testing.T) {
 		FirstName: "Alex",
 		Role:      "admin",
 	}
+	dict := map[string]interface{}{}
+	dict["firstName"] = "Alex"
+	dict["role"] = "admin"
 
 	// ---------------------------- Simple Style -------------------------------
 
@@ -57,6 +60,14 @@ func TestStyleParam(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, "firstName=Alex,role=admin", result)
 
+	result, err = StyleParam("simple", false, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, "firstName,Alex,role,admin", result)
+
+	result, err = StyleParam("simple", true, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, "firstName=Alex,role=admin", result)
+
 	// ----------------------------- Label Style -------------------------------
 
 	result, err = StyleParam("label", false, "id", primitive)
@@ -83,6 +94,14 @@ func TestStyleParam(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, ".firstName=Alex.role=admin", result)
 
+	result, err = StyleParam("label", false, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, ".firstName,Alex,role,admin", result)
+
+	result, err = StyleParam("label", true, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, ".firstName=Alex.role=admin", result)
+
 	// ----------------------------- Matrix Style ------------------------------
 
 	result, err = StyleParam("matrix", false, "id", primitive)
@@ -109,6 +128,14 @@ func TestStyleParam(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, ";firstName=Alex;role=admin", result)
 
+	result, err = StyleParam("matrix", false, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, ";id=firstName,Alex,role,admin", result)
+
+	result, err = StyleParam("matrix", true, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, ";firstName=Alex;role=admin", result)
+
 	// ------------------------------ Form Style -------------------------------
 	result, err = StyleParam("form", false, "id", primitive)
 	assert.NoError(t, err)
@@ -134,6 +161,14 @@ func TestStyleParam(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, "firstName=Alex&role=admin", result)
 
+	result, err = StyleParam("form", false, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, "id=firstName,Alex,role,admin", result)
+
+	result, err = StyleParam("form", true, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, "firstName=Alex&role=admin", result)
+
 	// ------------------------  spaceDelimited Style --------------------------
 
 	result, err = StyleParam("spaceDelimited", false, "id", primitive)
@@ -156,6 +191,11 @@ func TestStyleParam(t *testing.T) {
 	result, err = StyleParam("spaceDelimited", true, "id", object)
 	assert.Error(t, err)
 
+	result, err = StyleParam("spaceDelimited", false, "id", dict)
+	assert.Error(t, err)
+
+	result, err = StyleParam("spaceDelimited", true, "id", dict)
+	assert.Error(t, err)
 	// -------------------------  pipeDelimited Style --------------------------
 
 	result, err = StyleParam("pipeDelimited", false, "id", primitive)
@@ -178,6 +218,12 @@ func TestStyleParam(t *testing.T) {
 	result, err = StyleParam("pipeDelimited", true, "id", object)
 	assert.Error(t, err)
 
+	result, err = StyleParam("pipeDelimited", false, "id", dict)
+	assert.Error(t, err)
+
+	result, err = StyleParam("pipeDelimited", true, "id", dict)
+	assert.Error(t, err)
+
 	// ---------------------------  deepObject Style ---------------------------
 	result, err = StyleParam("deepObject", false, "id", primitive)
 	assert.Error(t, err)
@@ -198,6 +244,10 @@ func TestStyleParam(t *testing.T) {
 	assert.NoError(t, err)
 	assert.EqualValues(t, "id[firstName]=Alex&id[role]=admin", result)
 
+	result, err = StyleParam("deepObject", true, "id", dict)
+	assert.NoError(t, err)
+	assert.EqualValues(t, "id[firstName]=Alex&id[role]=admin", result)
+
 	// Misc tests
 	// Test type aliases
 	type StrType string