Repository: goadesign/goa
Pull request #3312 (2023-07-05 21:03:17)

Fix to handle time.Time slice and other slices as type override
Hello. A while ago I was working on this fix to handle custom types: https://github.com/goadesign/goa/pull/3010

That fix was originally intended to be a fix for use of `[]time.Time` but I changed it to `json.RawMessage` to make it easier to fix the protobuf tests. I didn't end up fixing the `[]time.Time` case and that issue is still preventing us from upgrading to newer versions of goa.

This PR fixes the issue for HTTP. It does not add to the tests. I tried to extend `types_dsl.go` but ran into protobuf errors that I couldn't figure out how to fix. Before proceeding I wanted to check:

1. Does this fix makes sense, is it worth fixing this use case for HTTP code generation.
2. Is it worth also making this fix in protobuf. Protobuf can't use `[]time.Time` in serialization, but maybe the `[]int` case could surface in protobuf (although imagine you'd just use `ArrayOf(Int)` and be done with it :/)?
3. If it's not worth making the protobuf fix, is there a suggestion for how I can write a test for this without hitting the protobuf code paths?

Repo here with a reproduction of the issue and some description of possible fixes: https://github.com/maxmcd/goa-timeslice-poc

cc: @vbrown608 who worked on this with me

edit: worth noting that you have basically answered this same question previously: https://github.com/goadesign/goa/pull/3010#issuecomment-1051081846, if the stance is still the same I'm down to get grpc custom types working as well, maybe just with a better test case this time


Patch:
diff --git a/Makefile b/Makefile
index 83f1bac2..365f9756 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@
 #
 MAJOR=3
 MINOR=12
-BUILD=1
+BUILD=2
 
 GOOS=$(shell go env GOOS)
 GO_FILES=$(shell find . -type f -name '*.go')
diff --git a/README.md b/README.md
index 0a7c0539..c5d45fa3 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 ## Overview
 
-[![GoDev](https://img.shields.io/badge/go-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/goa.design/goa/v3@v3.12.1/dsl?tab=doc)
+[![GoDev](https://img.shields.io/badge/go-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/goa.design/goa/v3@v3.12.2/dsl?tab=doc)
 [![Go Report Card](https://goreportcard.com/badge/goadesign/goa)](https://goreportcard.com/report/goadesign/goa)
 [![Slack](https://img.shields.io/badge/Goa-gray.svg?longCache=true&logo=slack&colorB=red)](https://gophers.slack.com/messages/goa)
 [![Signup](https://img.shields.io/badge/Signup-gray.svg?longCache=true&logo=slack&colorB=red)](https://invite.slack.golangbridge.org/)
@@ -67,7 +67,7 @@ invoking the client code.
 go install goa.design/goa/v3/cmd/goa@v3
 ```
 
-Current Release: `v3.12.1`
+Current Release: `v3.12.2`
 
 ## Teaser
 
@@ -333,7 +333,7 @@ In particular the page
 explains how to leverage the generated code to implement an HTTP or gRPC
 service.
 
-The [![DSL GoDoc](https://img.shields.io/badge/godoc-DSL-blue)](https://pkg.go.dev/goa.design/goa/v3@v3.12.1/dsl?tab=doc)
+The [![DSL GoDoc](https://img.shields.io/badge/godoc-DSL-blue)](https://pkg.go.dev/goa.design/goa/v3@v3.12.2/dsl?tab=doc)
 contains a fully documented reference of all the DSL functions.
 
 ### Instrumentation and System Example
diff --git a/codegen/funcs.go b/codegen/funcs.go
index f10dc49f..49ff5d49 100644
--- a/codegen/funcs.go
+++ b/codegen/funcs.go
@@ -403,6 +403,7 @@ var (
 		"RAM":   true,
 		"RHS":   true,
 		"RPC":   true,
+		"SDK":   true,
 		"SLA":   true,
 		"SMTP":  true,
 		"SQL":   true,
diff --git a/codegen/go_transform.go b/codegen/go_transform.go
index 26220648..fc29ce5b 100644
--- a/codegen/go_transform.go
+++ b/codegen/go_transform.go
@@ -279,14 +279,21 @@ func transformObject(source, target *expr.AttributeExpr, sourceVar, targetVar st
 				// (the field is not a pointer in this case)
 				code += "{\n\t"
 				if typeName, _ := GetMetaType(tgtc); typeName != "" {
-					code += fmt.Sprintf("var zero %s\n\t", typeName)
+					if !typeStringIsNilable(typeName) {
+						code += fmt.Sprintf("var zero %s\n\t", typeName)
+					}
 				} else if _, ok := tgtc.Type.(expr.UserType); ok {
 					// aliased primitive
 					code += fmt.Sprintf("var zero %s\n\t", ta.TargetCtx.Scope.Ref(tgtc, ta.TargetCtx.Pkg(tgtc)))
 				} else {
 					code += fmt.Sprintf("var zero %s\n\t", GoNativeTypeName(tgtc.Type))
 				}
-				code += fmt.Sprintf("if %s == zero {\n\t%s = %#v\n}\n", tgtVar, tgtVar, tdef)
+				if typeName, _ := GetMetaType(tgtc); typeName != "" && typeStringIsNilable(typeName) {
+					code += fmt.Sprintf("if %s == nil ", tgtVar)
+				} else {
+					code += fmt.Sprintf("if %s == zero ", tgtVar)
+				}
+				code += fmt.Sprintf("{\n\t%s = %#v\n}\n", tgtVar, tdef)
 				code += "}\n"
 			}
 		}
@@ -299,6 +306,12 @@ func transformObject(source, target *expr.AttributeExpr, sourceVar, targetVar st
 	return buffer.String(), nil
 }
 
+// typeStringIsNilable takes a go type as a string and checks for a '[]' or
+// 'map[' prefix to see if it's a nilable primitive type.
+func typeStringIsNilable(typeName string) bool {
+	return strings.HasPrefix(typeName, "[]") || strings.HasPrefix(typeName, "map[")
+}
+
 // transformArray generates Go code to transform source array to target array.
 func transformArray(source, target *expr.Array, sourceVar, targetVar string, newVar bool, ta *TransformAttrs) (string, error) {
 	if err := IsCompatible(source.ElemType.Type, target.ElemType.Type, sourceVar+"[0]", targetVar+"[0]"); err != nil {
diff --git a/go.mod b/go.mod
index 59b4a075..70c7c4fe 100644
--- a/go.mod
+++ b/go.mod
@@ -13,8 +13,8 @@ require (
 	github.com/sergi/go-diff v1.3.1
 	github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
 	golang.org/x/text v0.11.0
-	golang.org/x/tools v0.10.0
-	google.golang.org/grpc v1.56.1
+	golang.org/x/tools v0.11.0
+	google.golang.org/grpc v1.56.2
 	google.golang.org/protobuf v1.31.0
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -32,7 +32,7 @@ require (
 	github.com/perimeterx/marshmallow v1.1.4 // indirect
 	github.com/smartystreets/goconvey v1.7.2 // indirect
 	golang.org/x/mod v0.12.0 // indirect
-	golang.org/x/net v0.11.0 // indirect
+	golang.org/x/net v0.12.0 // indirect
 	golang.org/x/sys v0.10.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index c1d782af..ef5e6d63 100644
--- a/go.sum
+++ b/go.sum
@@ -85,8 +85,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
-golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
+golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
+golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
 golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
@@ -95,13 +95,13 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
 golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
-golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
+golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
+golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 h1:DEH99RbiLZhMxrpEJCZ0A+wdTe0EOgou/poSLx9vWf4=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
-google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ=
-google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
+google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
+google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
diff --git a/http/codegen/server.go b/http/codegen/server.go
index c25ff7f5..e26c5e9a 100644
--- a/http/codegen/server.go
+++ b/http/codegen/server.go
@@ -1010,10 +1010,10 @@ const typeConversionT = `{{- define "slice_conversion" }}
 			err = goa.MergeErrors(err, goa.InvalidFieldTypeError({{ printf "%q" .Name }}, {{ .VarName}}Raw, "integer"))
 		}
 		{{- if .Pointer }}
-		pv := int(v)
+		pv := {{ if .TypeRef }}{{slice .TypeRef 1 (len .TypeRef)}}{{ else }}int{{ end }}(v)
 		{{ .VarName }} = &pv
 		{{- else }}
-		{{ .VarName }} = int(v)
+		{{ .VarName }} = {{ if .TypeRef }}{{ .TypeRef }}{{ else }}int{{ end }}(v)
 		{{- end }}
 	{{- else if eq .Type.Name "int32" }}
 		v, err2 := strconv.ParseInt({{ .VarName }}Raw, 10, 32)
@@ -1021,27 +1021,27 @@ const typeConversionT = `{{- define "slice_conversion" }}
 			err = goa.MergeErrors(err, goa.InvalidFieldTypeError({{ printf "%q" .Name }}, {{ .VarName}}Raw, "integer"))
 		}
 		{{- if .Pointer }}
-		pv := int32(v)
+		pv := {{ if .TypeRef }}{{ slice .TypeRef 1 (len .TypeRef) }}{{ else }}int32{{ end }}(v)
 		{{ .VarName }} = &pv
 		{{- else }}
-		{{ .VarName }} = int32(v)
+		{{ .VarName }} = {{ if .TypeRef }}{{ .TypeRef }}{{ else }}int32{{ end }}(v)
 		{{- end }}
 	{{- else if eq .Type.Name "int64" }}
 		v, err2 := strconv.ParseInt({{ .VarName }}Raw, 10, 64)
 		if err2 != nil {
 			err = goa.MergeErrors(err, goa.InvalidFieldTypeError({{ printf "%q" .Name }}, {{ .VarName}}Raw, "integer"))
 		}
-		{{ .VarName }} = {{ if .Pointer}}&{{ end }}v
+		{{ if and (ne .TypeRef nil) (and (ne .TypeRef "int64") (ne .TypeRef "*int64")) }}{{ .VarName }} = ({{.TypeRef}})({{ if .Pointer }}&{{ end }}v){{ else }}{{ .VarName }} = {{ if .Pointer }}&{{ end }}v{{ end }}
 	{{- else if eq .Type.Name "uint" }}
 		v, err2 := strconv.ParseUint({{ .VarName }}Raw, 10, strconv.IntSize)
 		if err2 != nil {
 			err = goa.MergeErrors(err, goa.InvalidFieldTypeError({{ printf "%q" .Name }}, {{ .VarName}}Raw, "unsigned integer"))
 		}
 		{{- if .Pointer }}
-		pv := uint(v)
+		pv := {{ if .TypeRef }}{{ slice .TypeRef 1 (len .TypeRef) }}{{ else }}uint{{ end }}(v)
 		{{ .VarName }} = &pv
 		{{- else }}
-		{{ .VarName }} = uint(v)
+		{{ .VarName }} = {{ if .TypeRef }}{{ .TypeRef }}{{ else }}uint{{ end }}(v)
 		{{- end }}
 	{{- else if eq .Type.Name "uint32" }}
 		v, err2 := strconv.ParseUint({{ .VarName }}Raw, 10, 32)
@@ -1049,40 +1049,40 @@ const typeConversionT = `{{- define "slice_conversion" }}
 			err = goa.MergeErrors(err, goa.InvalidFieldTypeError({{ printf "%q" .Name }}, {{ .VarName}}Raw, "unsigned integer"))
 		}
 		{{- if .Pointer }}
-		pv := uint32(v)
+		pv := {{ if .TypeRef }}{{ slice .TypeRef 1 (len .TypeRef) }}{{ else }}uint32{{ end }}(v)
 		{{ .VarName }} = &pv
 		{{- else }}
-		{{ .VarName }} = uint32(v)
+		{{ .VarName }} = {{ if .TypeRef }}{{ .TypeRef }}{{ else }}uint32{{ end }}(v)
 		{{- end }}
 	{{- else if eq .Type.Name "uint64" }}
 		v, err2 := strconv.ParseUint({{ .VarName }}Raw, 10, 64)
 		if err2 != nil {
 			err = goa.MergeErrors(err, goa.InvalidFieldTypeError({{ printf "%q" .Name }}, {{ .VarName}}Raw, "unsigned integer"))
 		}
-		{{ .VarName }} = {{ if .Pointer }}&{{ end }}v
+		{{ if and (ne .TypeRef nil) (and (ne .TypeRef "uint64") (ne .TypeRef "*uint64")) }}{{ .VarName }} = ({{.TypeRef}})({{ if .Pointer }}&{{ end }}v){{ else }}{{ .VarName }} = {{ if .Pointer }}&{{ end }}v{{ end }}
 	{{- else if eq .Type.Name "float32" }}
 		v, err2 := strconv.ParseFloat({{ .VarName }}Raw, 32)
 		if err2 != nil {
 			err = goa.MergeErrors(err, goa.InvalidFieldTypeError({{ printf "%q" .Name }}, {{ .VarName}}Raw, "float"))
 		}
 		{{- if .Pointer }}
-		pv := float32(v)
+		pv := {{ if .TypeRef }}{{ slice .TypeRef 1 (len .TypeRef) }}{{ else }}float32{{ end }}(v)
 		{{ .VarName }} = &pv
 		{{- else }}
-		{{ .VarName }} = float32(v)
+		{{ .VarName }} = {{ if .TypeRef }}{{ .TypeRef }}{{ else }}float32{{ end }}(v)
 		{{- end }}
 	{{- else if eq .Type.Name "float64" }}
 		v, err2 := strconv.ParseFloat({{ .VarName }}Raw, 64)
 		if err2 != nil {
 			err = goa.MergeErrors(err, goa.InvalidFieldTypeError({{ printf "%q" .Name }}, {{ .VarName}}Raw, "float"))
 		}
-		{{ .VarName }} = {{ if .Pointer }}&{{ end }}v
+		{{ if and (ne .TypeRef nil) (and (ne .TypeRef "float64") (ne .TypeRef "*float64")) }}{{ .VarName }} = ({{.TypeRef}})({{ if .Pointer }}&{{ end }}v){{ else }}{{ .VarName }} = {{ if .Pointer }}&{{ end }}v{{ end }}
 	{{- else if eq .Type.Name "boolean" }}
 		v, err2 := strconv.ParseBool({{ .VarName }}Raw)
 		if err2 != nil {
 			err = goa.MergeErrors(err, goa.InvalidFieldTypeError({{ printf "%q" .Name }}, {{ .VarName}}Raw, "boolean"))
 		}
-		{{ .VarName }} = {{ if .Pointer }}&{{ end }}v
+		{{ if and (ne .TypeRef nil) (and (ne .TypeRef "bool") (ne .TypeRef "*bool")) }}{{ .VarName }} = ({{.TypeRef}})({{ if .Pointer }}&{{ end }}v){{ else }}{{ .VarName }} = {{ if .Pointer }}&{{ end }}v{{ end }}
 	{{- else }}
 		// unsupported type {{ .Type.Name }} for var {{ .VarName }}
 	{{- end }}
diff --git a/pkg/version.go b/pkg/version.go
index c6db1ac7..133663d4 100644
--- a/pkg/version.go
+++ b/pkg/version.go
@@ -12,7 +12,7 @@ const (
 	// Minor version number
 	Minor = 12
 	// Build number
-	Build = 1
+	Build = 2
 	// Suffix - set to empty string in release tag commits.
 	Suffix = ""
 )


Test patch:
diff --git a/http/codegen/server_decode_test.go b/http/codegen/server_decode_test.go
index ef9b0e43..9b0a1bc3 100644
--- a/http/codegen/server_decode_test.go
+++ b/http/codegen/server_decode_test.go
@@ -15,6 +15,14 @@ func TestDecode(t *testing.T) {
 		DSL  func()
 		Code string
 	}{
+		{"decode-path-custom-float32", testdata.PayloadPathCustomFloat32DSL, testdata.PayloadPathCustomFloat32DecodeCode},
+		{"decode-path-custom-float64", testdata.PayloadPathCustomFloat64DSL, testdata.PayloadPathCustomFloat64DecodeCode},
+		{"decode-path-custom-int", testdata.PayloadPathCustomIntDSL, testdata.PayloadPathCustomIntDecodeCode},
+		{"decode-path-custom-int32", testdata.PayloadPathCustomInt32DSL, testdata.PayloadPathCustomInt32DecodeCode},
+		{"decode-path-custom-int64", testdata.PayloadPathCustomInt64DSL, testdata.PayloadPathCustomInt64DecodeCode},
+		{"decode-path-custom-uint", testdata.PayloadPathCustomUIntDSL, testdata.PayloadPathCustomUIntDecodeCode},
+		{"decode-path-custom-uint32", testdata.PayloadPathCustomUInt32DSL, testdata.PayloadPathCustomUInt32DecodeCode},
+		{"decode-path-custom-uint64", testdata.PayloadPathCustomUInt64DSL, testdata.PayloadPathCustomUInt64DecodeCode},
 		{"decode-query-bool", testdata.PayloadQueryBoolDSL, testdata.PayloadQueryBoolDecodeCode},
 		{"decode-query-bool-validate", testdata.PayloadQueryBoolValidateDSL, testdata.PayloadQueryBoolValidateDecodeCode},
 		{"decode-query-int", testdata.PayloadQueryIntDSL, testdata.PayloadQueryIntDecodeCode},
diff --git a/http/codegen/testdata/payload_decode_functions.go b/http/codegen/testdata/payload_decode_functions.go
index e52f8065..72ba9425 100644
--- a/http/codegen/testdata/payload_decode_functions.go
+++ b/http/codegen/testdata/payload_decode_functions.go
@@ -5497,3 +5497,223 @@ func DecodeMethodARequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp
 	}
 }
 `
+
+var PayloadPathCustomFloat32DecodeCode = `// DecodeMethodPathCustomFloat32Request returns a decoder for requests sent to
+// the ServicePathCustomFloat32 MethodPathCustomFloat32 endpoint.
+func DecodeMethodPathCustomFloat32Request(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) {
+	return func(r *http.Request) (any, error) {
+		var (
+			p   hide.Float32
+			err error
+
+			params = mux.Vars(r)
+		)
+		{
+			pRaw := params["p"]
+			v, err2 := strconv.ParseFloat(pRaw, 32)
+			if err2 != nil {
+				err = goa.MergeErrors(err, goa.InvalidFieldTypeError("p", pRaw, "float"))
+			}
+			p = hide.Float32(v)
+		}
+		if err != nil {
+			return nil, err
+		}
+		payload := NewMethodPathCustomFloat32Payload(p)
+
+		return payload, nil
+	}
+}
+`
+
+var PayloadPathCustomFloat64DecodeCode = `// DecodeMethodPathCustomFloat64Request returns a decoder for requests sent to
+// the ServicePathCustomFloat64 MethodPathCustomFloat64 endpoint.
+func DecodeMethodPathCustomFloat64Request(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) {
+	return func(r *http.Request) (any, error) {
+		var (
+			p   hide.Float64
+			err error
+
+			params = mux.Vars(r)
+		)
+		{
+			pRaw := params["p"]
+			v, err2 := strconv.ParseFloat(pRaw, 64)
+			if err2 != nil {
+				err = goa.MergeErrors(err, goa.InvalidFieldTypeError("p", pRaw, "float"))
+			}
+			p = (hide.Float64)(v)
+		}
+		if err != nil {
+			return nil, err
+		}
+		payload := NewMethodPathCustomFloat64Payload(p)
+
+		return payload, nil
+	}
+}
+`
+
+var PayloadPathCustomIntDecodeCode = `// DecodeMethodPathCustomIntRequest returns a decoder for requests sent to the
+// ServicePathCustomInt MethodPathCustomInt endpoint.
+func DecodeMethodPathCustomIntRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) {
+	return func(r *http.Request) (any, error) {
+		var (
+			p   hide.Int
+			err error
+
+			params = mux.Vars(r)
+		)
+		{
+			pRaw := params["p"]
+			v, err2 := strconv.ParseInt(pRaw, 10, strconv.IntSize)
+			if err2 != nil {
+				err = goa.MergeErrors(err, goa.InvalidFieldTypeError("p", pRaw, "integer"))
+			}
+			p = hide.Int(v)
+		}
+		if err != nil {
+			return nil, err
+		}
+		payload := NewMethodPathCustomIntPayload(p)
+
+		return payload, nil
+	}
+}
+`
+
+var PayloadPathCustomInt32DecodeCode = `// DecodeMethodPathCustomInt32Request returns a decoder for requests sent to
+// the ServicePathCustomInt32 MethodPathCustomInt32 endpoint.
+func DecodeMethodPathCustomInt32Request(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) {
+	return func(r *http.Request) (any, error) {
+		var (
+			p   hide.Int32
+			err error
+
+			params = mux.Vars(r)
+		)
+		{
+			pRaw := params["p"]
+			v, err2 := strconv.ParseInt(pRaw, 10, 32)
+			if err2 != nil {
+				err = goa.MergeErrors(err, goa.InvalidFieldTypeError("p", pRaw, "integer"))
+			}
+			p = hide.Int32(v)
+		}
+		if err != nil {
+			return nil, err
+		}
+		payload := NewMethodPathCustomInt32Payload(p)
+
+		return payload, nil
+	}
+}
+`
+var PayloadPathCustomInt64DecodeCode = `// DecodeMethodPathCustomInt64Request returns a decoder for requests sent to
+// the ServicePathCustomInt64 MethodPathCustomInt64 endpoint.
+func DecodeMethodPathCustomInt64Request(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) {
+	return func(r *http.Request) (any, error) {
+		var (
+			p   hide.Int64
+			err error
+
+			params = mux.Vars(r)
+		)
+		{
+			pRaw := params["p"]
+			v, err2 := strconv.ParseInt(pRaw, 10, 64)
+			if err2 != nil {
+				err = goa.MergeErrors(err, goa.InvalidFieldTypeError("p", pRaw, "integer"))
+			}
+			p = (hide.Int64)(v)
+		}
+		if err != nil {
+			return nil, err
+		}
+		payload := NewMethodPathCustomInt64Payload(p)
+
+		return payload, nil
+	}
+}
+`
+var PayloadPathCustomUIntDecodeCode = `// DecodeMethodPathCustomUIntRequest returns a decoder for requests sent to the
+// ServicePathCustomUInt MethodPathCustomUInt endpoint.
+func DecodeMethodPathCustomUIntRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) {
+	return func(r *http.Request) (any, error) {
+		var (
+			p   hide.Uint
+			err error
+
+			params = mux.Vars(r)
+		)
+		{
+			pRaw := params["p"]
+			v, err2 := strconv.ParseUint(pRaw, 10, strconv.IntSize)
+			if err2 != nil {
+				err = goa.MergeErrors(err, goa.InvalidFieldTypeError("p", pRaw, "unsigned integer"))
+			}
+			p = hide.Uint(v)
+		}
+		if err != nil {
+			return nil, err
+		}
+		payload := NewMethodPathCustomUIntPayload(p)
+
+		return payload, nil
+	}
+}
+`
+var PayloadPathCustomUInt32DecodeCode = `// DecodeMethodPathCustomUInt32Request returns a decoder for requests sent to
+// the ServicePathCustomUInt32 MethodPathCustomUInt32 endpoint.
+func DecodeMethodPathCustomUInt32Request(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) {
+	return func(r *http.Request) (any, error) {
+		var (
+			p   hide.Uint32
+			err error
+
+			params = mux.Vars(r)
+		)
+		{
+			pRaw := params["p"]
+			v, err2 := strconv.ParseUint(pRaw, 10, 32)
+			if err2 != nil {
+				err = goa.MergeErrors(err, goa.InvalidFieldTypeError("p", pRaw, "unsigned integer"))
+			}
+			p = hide.Uint32(v)
+		}
+		if err != nil {
+			return nil, err
+		}
+		payload := NewMethodPathCustomUInt32Payload(p)
+
+		return payload, nil
+	}
+}
+`
+var PayloadPathCustomUInt64DecodeCode = `// DecodeMethodPathCustomUInt64Request returns a decoder for requests sent to
+// the ServicePathCustomUInt64 MethodPathCustomUInt64 endpoint.
+func DecodeMethodPathCustomUInt64Request(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) {
+	return func(r *http.Request) (any, error) {
+		var (
+			p   hide.Uint64
+			err error
+
+			params = mux.Vars(r)
+		)
+		{
+			pRaw := params["p"]
+			v, err2 := strconv.ParseUint(pRaw, 10, 64)
+			if err2 != nil {
+				err = goa.MergeErrors(err, goa.InvalidFieldTypeError("p", pRaw, "unsigned integer"))
+			}
+			p = (hide.Uint64)(v)
+		}
+		if err != nil {
+			return nil, err
+		}
+		payload := NewMethodPathCustomUInt64Payload(p)
+
+		return payload, nil
+	}
+}
+`
diff --git a/http/codegen/testdata/payload_dsls.go b/http/codegen/testdata/payload_dsls.go
index 678f2572..72622b52 100644
--- a/http/codegen/testdata/payload_dsls.go
+++ b/http/codegen/testdata/payload_dsls.go
@@ -3380,3 +3380,123 @@ var QueryArrayNestedAliasValidateDSL = func() {
 		})
 	})
 }
+
+var PayloadPathCustomFloat32DSL = func() {
+	Service("ServicePathCustomFloat32", func() {
+		Method("MethodPathCustomFloat32", func() {
+			Payload(func() {
+				Attribute("p", Float32, func() {
+					Meta("struct:field:type", "hide.Float32", "github.com/c2h5oh/hide")
+				})
+			})
+			HTTP(func() {
+				GET("/{p}")
+			})
+		})
+	})
+}
+
+var PayloadPathCustomFloat64DSL = func() {
+	Service("ServicePathCustomFloat64", func() {
+		Method("MethodPathCustomFloat64", func() {
+			Payload(func() {
+				Attribute("p", Float64, func() {
+					Meta("struct:field:type", "hide.Float64", "github.com/c2h5oh/hide")
+				})
+			})
+			HTTP(func() {
+				GET("/{p}")
+			})
+		})
+	})
+}
+
+var PayloadPathCustomIntDSL = func() {
+	Service("ServicePathCustomInt", func() {
+		Method("MethodPathCustomInt", func() {
+			Payload(func() {
+				Attribute("p", Int, func() {
+					Meta("struct:field:type", "hide.Int", "github.com/c2h5oh/hide")
+				})
+			})
+			HTTP(func() {
+				GET("/{p}")
+			})
+		})
+	})
+}
+
+var PayloadPathCustomInt32DSL = func() {
+	Service("ServicePathCustomInt32", func() {
+		Method("MethodPathCustomInt32", func() {
+			Payload(func() {
+				Attribute("p", Int32, func() {
+					Meta("struct:field:type", "hide.Int32", "github.com/c2h5oh/hide")
+				})
+			})
+			HTTP(func() {
+				GET("/{p}")
+			})
+		})
+	})
+}
+
+var PayloadPathCustomInt64DSL = func() {
+	Service("ServicePathCustomInt64", func() {
+		Method("MethodPathCustomInt64", func() {
+			Payload(func() {
+				Attribute("p", Int64, func() {
+					Meta("struct:field:type", "hide.Int64", "github.com/c2h5oh/hide")
+				})
+			})
+			HTTP(func() {
+				GET("/{p}")
+			})
+		})
+	})
+}
+
+var PayloadPathCustomUIntDSL = func() {
+	Service("ServicePathCustomUInt", func() {
+		Method("MethodPathCustomUInt", func() {
+			Payload(func() {
+				Attribute("p", UInt, func() {
+					Meta("struct:field:type", "hide.Uint", "github.com/c2h5oh/hide")
+				})
+			})
+			HTTP(func() {
+				GET("/{p}")
+			})
+		})
+	})
+}
+
+var PayloadPathCustomUInt32DSL = func() {
+	Service("ServicePathCustomUInt32", func() {
+		Method("MethodPathCustomUInt32", func() {
+			Payload(func() {
+				Attribute("p", UInt32, func() {
+					Meta("struct:field:type", "hide.Uint32", "github.com/c2h5oh/hide")
+				})
+			})
+			HTTP(func() {
+				GET("/{p}")
+			})
+		})
+	})
+}
+
+var PayloadPathCustomUInt64DSL = func() {
+	Service("ServicePathCustomUInt64", func() {
+		Method("MethodPathCustomUInt64", func() {
+			Payload(func() {
+				Attribute("p", UInt64, func() {
+					Meta("struct:field:type", "hide.Uint64", "github.com/c2h5oh/hide")
+				})
+			})
+			HTTP(func() {
+				GET("/{p}")
+			})
+		})
+	})
+}