Repository: ogen-go/ogen
Pull request #730 (2022-12-22 09:55:50)

feat(gen): allow to define custom format for primitive types


Patch:
diff --git a/gen/custom_format.go b/gen/custom_format.go
new file mode 100644
index 00000000..c4e83096
--- /dev/null
+++ b/gen/custom_format.go
@@ -0,0 +1,106 @@
+package gen
+
+import (
+	"fmt"
+	"go/token"
+	"reflect"
+
+	"github.com/go-faster/errors"
+
+	"github.com/ogen-go/ogen/gen/ir"
+	"github.com/ogen-go/ogen/internal/xmaps"
+)
+
+func checkImportableType(typ reflect.Type) error {
+	path := typ.PkgPath()
+	if path == "main" {
+		return errors.New("type must be in importable package")
+	}
+
+	name := typ.Name()
+	if name == "" {
+		return errors.New("type must be named or primitive")
+	}
+
+	if path != "" && !token.IsExported(name) {
+		return errors.New("type must be exported")
+	}
+
+	return nil
+}
+
+func (g *Generator) makeCustomFormats() error {
+	importPaths := map[string]string{}
+
+	makeExternal := func(typ reflect.Type) (ir.ExternalType, error) {
+		if err := checkImportableType(typ); err != nil {
+			return ir.ExternalType{}, err
+		}
+
+		path := typ.PkgPath()
+		if path == "" {
+			// Primitive type.
+			return ir.ExternalType{Type: typ}, nil
+		}
+
+		importName, ok := importPaths[path]
+		if !ok {
+			importName = fmt.Sprintf("custom%d", len(importPaths))
+			importPaths[path] = importName
+			g.imports = append(g.imports, fmt.Sprintf("%s %q", importName, path))
+		}
+
+		return ir.ExternalType{
+			Pkg:  importName,
+			Type: typ,
+		}, nil
+	}
+
+	for _, jsonTyp := range xmaps.SortedKeys(g.opt.CustomFormats) {
+		formats := g.opt.CustomFormats[jsonTyp]
+		for _, format := range xmaps.SortedKeys(formats) {
+			def := formats[format]
+
+			if _, ok := g.customFormats[jsonTyp]; !ok {
+				g.customFormats[jsonTyp] = map[string]ir.CustomFormat{}
+			}
+
+			f, err := func() (f ir.CustomFormat, _ error) {
+				goName, err := pascalNonEmpty(format)
+				if err != nil {
+					return f, errors.Wrap(err, "generate go name")
+				}
+
+				typ, err := makeExternal(def.typ)
+				if err != nil {
+					return f, errors.Wrap(err, "format type")
+				}
+
+				json, err := makeExternal(def.json)
+				if err != nil {
+					return f, errors.Wrap(err, "json encoding")
+				}
+
+				text, err := makeExternal(def.text)
+				if err != nil {
+					return f, errors.Wrap(err, "text encoding")
+				}
+
+				return ir.CustomFormat{
+					Name:   format,
+					GoName: goName,
+					Type:   typ,
+					JSON:   json,
+					Text:   text,
+				}, nil
+			}()
+			if err != nil {
+				return errors.Wrapf(err, "custom format %q:%q", jsonTyp, format)
+			}
+
+			g.customFormats[jsonTyp][format] = f
+		}
+	}
+
+	return nil
+}
diff --git a/gen/generator.go b/gen/generator.go
index faab9dbb..1a790e13 100644
--- a/gen/generator.go
+++ b/gen/generator.go
@@ -1,9 +1,6 @@
 package gen
 
 import (
-	"fmt"
-	"go/token"
-	"reflect"
 	"strings"
 
 	"github.com/go-faster/errors"
@@ -12,7 +9,6 @@ import (
 
 	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/gen/ir"
-	"github.com/ogen-go/ogen/internal/xmaps"
 	"github.com/ogen-go/ogen/internal/xslices"
 	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/openapi"
@@ -86,80 +82,6 @@ func NewGenerator(spec *ogen.Spec, opts Options) (*Generator, error) {
 	return g, nil
 }
 
-func (g *Generator) makeCustomFormats() error {
-	importPaths := map[string]string{}
-
-	makeExternal := func(typ reflect.Type) (ir.ExternalType, error) {
-		path := typ.PkgPath()
-		if path == "main" {
-			return ir.ExternalType{}, errors.New("type must be in importable package")
-		}
-		if n := typ.Name(); n == "" || !token.IsExported(n) {
-			return ir.ExternalType{}, errors.New("type must be named and exported")
-		}
-
-		importName, ok := importPaths[path]
-		if !ok {
-			importName = fmt.Sprintf("custom%d", len(importPaths))
-			importPaths[path] = importName
-			g.imports = append(g.imports, fmt.Sprintf("%s %q", importName, path))
-		}
-
-		return ir.ExternalType{
-			Pkg:  importName,
-			Type: typ,
-		}, nil
-	}
-
-	for _, jsonTyp := range xmaps.SortedKeys(g.opt.CustomFormats) {
-		formats := g.opt.CustomFormats[jsonTyp]
-		for _, format := range xmaps.SortedKeys(formats) {
-			def := formats[format]
-
-			if _, ok := g.customFormats[jsonTyp]; !ok {
-				g.customFormats[jsonTyp] = map[string]ir.CustomFormat{}
-			}
-
-			f, err := func() (f ir.CustomFormat, _ error) {
-				goName, err := pascalNonEmpty(format)
-				if err != nil {
-					return f, errors.Wrap(err, "generate go name")
-				}
-
-				typ, err := makeExternal(def.typ)
-				if err != nil {
-					return f, errors.Wrap(err, "format type")
-				}
-
-				json, err := makeExternal(def.json)
-				if err != nil {
-					return f, errors.Wrap(err, "json encoding")
-				}
-
-				text, err := makeExternal(def.text)
-				if err != nil {
-					return f, errors.Wrap(err, "text encoding")
-				}
-
-				return ir.CustomFormat{
-					Name:   format,
-					GoName: goName,
-					Type:   typ,
-					JSON:   json,
-					Text:   text,
-				}, nil
-			}()
-			if err != nil {
-				return errors.Wrapf(err, "custom format %q:%q", jsonTyp, format)
-			}
-
-			g.customFormats[jsonTyp][format] = f
-		}
-	}
-
-	return nil
-}
-
 func (g *Generator) makeIR(api *openapi.API) error {
 	if err := g.makeServers(api.Servers); err != nil {
 		return errors.Wrap(err, "servers")
diff --git a/gen/ir/custom_format.go b/gen/ir/custom_format.go
index 03ded62a..2af3d5d5 100644
--- a/gen/ir/custom_format.go
+++ b/gen/ir/custom_format.go
@@ -14,6 +14,10 @@ type ExternalType struct {
 
 // Go returns valid Go type for this ExternalType.
 func (c ExternalType) Go() string {
+	if c.Pkg == "" {
+		// Primitive type.
+		return c.Type.Name()
+	}
 	return fmt.Sprintf("%s.%s", c.Pkg, c.Type.Name())
 }
 
diff --git a/gen/schema_gen.go b/gen/schema_gen.go
index cc7737fa..ee51e6c5 100644
--- a/gen/schema_gen.go
+++ b/gen/schema_gen.go
@@ -281,15 +281,17 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T
 			if err := t.Validators.SetString(schema); err != nil {
 				return nil, errors.Wrap(err, "string validator")
 			}
-			switch t.Primitive {
-			case ir.String, ir.ByteSlice:
-			default:
-				g.log.Warn("String validator cannot be applied to non-string type and will be ignored",
-					zapPosition(schema),
-					zap.String("type", string(schema.Type)),
-					zap.String("format", schema.Format),
-					zap.String("go_type", t.Go()),
-				)
+			if t.Validators.String.Set() {
+				switch t.Primitive {
+				case ir.String, ir.ByteSlice:
+				default:
+					g.log.Warn("String validator cannot be applied to non-string type and will be ignored",
+						zapPosition(schema),
+						zap.String("type", string(schema.Type)),
+						zap.String("format", schema.Format),
+						zap.String("go_type", t.Go()),
+					)
+				}
 			}
 		case jsonschema.Integer:
 			if err := t.Validators.SetInt(schema); err != nil {
diff --git a/internal/integration/cmd/customformats/main.go b/internal/integration/cmd/customformats/main.go
index dd6bc8fb..117b50e8 100644
--- a/internal/integration/cmd/customformats/main.go
+++ b/internal/integration/cmd/customformats/main.go
@@ -11,6 +11,7 @@ import (
 	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/gen"
 	"github.com/ogen-go/ogen/gen/genfs"
+	"github.com/ogen-go/ogen/internal/integration/customformats/hextype"
 	"github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
 	"github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
 	"github.com/ogen-go/ogen/internal/location"
@@ -53,16 +54,9 @@ func run(specPath, targetDir string) error {
 		RootURL: u,
 		CustomFormats: gen.CustomFormatsMap{
 			jsonschema.String: {
-				"phone": gen.CustomFormat[
-					phonetype.Phone,
-					phonetype.JSONPhoneEncoding,
-					phonetype.TextPhoneEncoding,
-				](),
-				"rgba": gen.CustomFormat[
-					rgbatype.RGBA,
-					rgbatype.JSONRGBAEncoding,
-					rgbatype.TextRGBAEncoding,
-				](),
+				"phone": phonetype.PhoneFormat,
+				"rgba":  rgbatype.RGBAFormat,
+				"hex":   hextype.HexFormat,
 			},
 		},
 		File:   location.NewFile(fileName, specPath, data),
diff --git a/internal/integration/customformats/hextype/hex.go b/internal/integration/customformats/hextype/hex.go
new file mode 100644
index 00000000..4f6ff2eb
--- /dev/null
+++ b/internal/integration/customformats/hextype/hex.go
@@ -0,0 +1,49 @@
+// Package hextype defines a custom format for hexadecimal numbers.
+package hextype
+
+import (
+	"strconv"
+
+	"github.com/go-faster/jx"
+
+	"github.com/ogen-go/ogen/gen"
+)
+
+// HexFormat defines a custom format for hexadecimal numbers.
+var HexFormat = gen.CustomFormat[
+	int64,
+	JSONHexEncoding,
+	TextHexEncoding,
+]()
+
+// JSONHexEncoding defines a custom JSON encoding for hexadecimal numbers.
+type JSONHexEncoding struct{}
+
+// EncodeJSON encodes a hexadecimal number as a JSON string.
+func (JSONHexEncoding) EncodeJSON(e *jx.Encoder, v int64) {
+	var t TextHexEncoding
+	e.Str(t.EncodeText(v))
+}
+
+// DecodeJSON decodes a hexadecimal number from a JSON string.
+func (JSONHexEncoding) DecodeJSON(d *jx.Decoder) (v int64, _ error) {
+	s, err := d.Str()
+	if err != nil {
+		return v, err
+	}
+	var t TextHexEncoding
+	return t.DecodeText(s)
+}
+
+// TextHexEncoding defines a custom text encoding for hexadecimal numbers.
+type TextHexEncoding struct{}
+
+// EncodeText encodes a hexadecimal number as a string.
+func (TextHexEncoding) EncodeText(v int64) string {
+	return strconv.FormatInt(v, 16)
+}
+
+// DecodeText decodes a hexadecimal number from a string.
+func (TextHexEncoding) DecodeText(s string) (v int64, _ error) {
+	return strconv.ParseInt(s, 16, 64)
+}
diff --git a/internal/integration/customformats/phonetype/phone.go b/internal/integration/customformats/phonetype/phone.go
index 3536cc32..a06a4afa 100644
--- a/internal/integration/customformats/phonetype/phone.go
+++ b/internal/integration/customformats/phonetype/phone.go
@@ -6,19 +6,32 @@ import (
 
 	"github.com/go-faster/errors"
 	"github.com/go-faster/jx"
+
+	"github.com/ogen-go/ogen/gen"
 )
 
+// PhoneFormat defines a custom format for phone numbers.
+var PhoneFormat = gen.CustomFormat[
+	Phone,
+	JSONPhoneEncoding,
+	TextPhoneEncoding,
+]()
+
+// Phone is a phone number.
 type Phone string
 
 var phoneRegexp = regexp.MustCompile(`^\+\d+$`)
 
+// JSONPhoneEncoding defines a custom JSON encoding for phone numbers.
 type JSONPhoneEncoding struct{}
 
+// EncodeJSON encodes a phone number as a JSON string.
 func (JSONPhoneEncoding) EncodeJSON(e *jx.Encoder, v Phone) {
 	var t TextPhoneEncoding
 	e.Str(t.EncodeText(v))
 }
 
+// DecodeJSON decodes a phone number from a JSON string.
 func (JSONPhoneEncoding) DecodeJSON(d *jx.Decoder) (v Phone, _ error) {
 	s, err := d.Str()
 	if err != nil {
@@ -28,12 +41,15 @@ func (JSONPhoneEncoding) DecodeJSON(d *jx.Decoder) (v Phone, _ error) {
 	return t.DecodeText(s)
 }
 
+// TextPhoneEncoding defines a custom text encoding for phone numbers.
 type TextPhoneEncoding struct{}
 
+// EncodeText encodes a phone number as a string.
 func (TextPhoneEncoding) EncodeText(v Phone) string {
 	return string(v)
 }
 
+// DecodeText decodes a phone number from a string.
 func (TextPhoneEncoding) DecodeText(s string) (v Phone, _ error) {
 	if !phoneRegexp.MatchString(s) {
 		return v, errors.Errorf("invalid phone %q", s)
diff --git a/internal/integration/customformats/rgbatype/rgba.go b/internal/integration/customformats/rgbatype/rgba.go
index 9809ff6a..862d7b13 100644
--- a/internal/integration/customformats/rgbatype/rgba.go
+++ b/internal/integration/customformats/rgbatype/rgba.go
@@ -5,19 +5,32 @@ import (
 	"fmt"
 
 	"github.com/go-faster/jx"
+
+	"github.com/ogen-go/ogen/gen"
 )
 
+// RGBAFormat defines a custom format for RGBA colors.
+var RGBAFormat = gen.CustomFormat[
+	RGBA,
+	JSONRGBAEncoding,
+	TextRGBAEncoding,
+]()
+
+// RGBA is a color with red, green, blue, and alpha components.
 type RGBA struct {
 	R, G, B, A uint8
 }
 
+// JSONRGBAEncoding defines a custom JSON encoding for RGBA colors.
 type JSONRGBAEncoding struct{}
 
+// EncodeJSON encodes an RGBA color as a JSON string.
 func (JSONRGBAEncoding) EncodeJSON(e *jx.Encoder, v RGBA) {
 	var t TextRGBAEncoding
 	e.Str(t.EncodeText(v))
 }
 
+// DecodeJSON decodes an RGBA color from a JSON string.
 func (JSONRGBAEncoding) DecodeJSON(d *jx.Decoder) (v RGBA, _ error) {
 	s, err := d.Str()
 	if err != nil {
@@ -27,12 +40,15 @@ func (JSONRGBAEncoding) DecodeJSON(d *jx.Decoder) (v RGBA, _ error) {
 	return t.DecodeText(s)
 }
 
+// TextRGBAEncoding defines a custom text encoding for RGBA colors.
 type TextRGBAEncoding struct{}
 
+// EncodeText encodes an RGBA color as a string.
 func (TextRGBAEncoding) EncodeText(v RGBA) string {
 	return fmt.Sprintf("rgba(%d,%d,%d,%d)", v.R, v.G, v.B, v.A)
 }
 
+// DecodeText decodes an RGBA color from a string.
 func (TextRGBAEncoding) DecodeText(s string) (v RGBA, _ error) {
 	_, err := fmt.Sscanf(s, "rgba(%d,%d,%d,%d)", &v.R, &v.G, &v.B, &v.A)
 	return v, err


Test patch:
diff --git a/_testdata/positive/custom_formats.json b/_testdata/positive/custom_formats.json
index f13d0ad3..cc8201e1 100644
--- a/_testdata/positive/custom_formats.json
+++ b/_testdata/positive/custom_formats.json
@@ -13,6 +13,9 @@
           },
           {
             "$ref": "#/components/parameters/Color"
+          },
+          {
+            "$ref": "#/components/parameters/Hex"
           }
         ],
         "requestBody": {
@@ -60,6 +63,15 @@
           "type": "string",
           "format": "rgba"
         }
+      },
+      "Hex": {
+        "name": "hex",
+        "in": "query",
+        "description": "Hex",
+        "schema": {
+          "type": "string",
+          "format": "hex"
+        }
       }
     },
     "schemas": {
@@ -90,6 +102,10 @@
           "background_color": {
             "type": "string",
             "format": "rgba"
+          },
+          "hex_color": {
+            "type": "string",
+            "format": "hex"
           }
         }
       }
diff --git a/gen/custom_format_test.go b/gen/custom_format_test.go
new file mode 100644
index 00000000..63c36ee0
--- /dev/null
+++ b/gen/custom_format_test.go
@@ -0,0 +1,87 @@
+package gen
+
+import (
+	"fmt"
+	"reflect"
+	"testing"
+	"unsafe"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/ogen-go/ogen/gen/ir"
+)
+
+func typeOf[T any]() reflect.Type {
+	return reflect.TypeOf(new(T)).Elem()
+}
+
+type foo struct{}
+
+type ExportedFunc func() foo
+
+func Test_checkImportableType(t *testing.T) {
+	tests := []struct {
+		typ     reflect.Type
+		wantErr bool
+	}{
+		// Primitive types.
+		{typeOf[bool](), false},
+		{typeOf[int](), false},
+		{typeOf[int8](), false},
+		{typeOf[int16](), false},
+		{typeOf[int32](), false},
+		{typeOf[int64](), false},
+		{typeOf[uint](), false},
+		{typeOf[uint8](), false},
+		{typeOf[uint16](), false},
+		{typeOf[uint32](), false},
+		{typeOf[uint64](), false},
+		{typeOf[uintptr](), false},
+		{typeOf[float32](), false},
+		{typeOf[float64](), false},
+		{typeOf[complex64](), false},
+		{typeOf[complex128](), false},
+		{typeOf[string](), false},
+		{typeOf[unsafe.Pointer](), false},
+		// Exported types.
+		{typeOf[Generator](), false},
+		{typeOf[ir.Kind](), false},
+		{typeOf[ir.CustomFormat](), false},
+		{typeOf[ExportedFunc](), false},
+
+		// Negative cases.
+		//
+		// Unexported type.
+		{typeOf[foo](), true},
+		{typeOf[func(foo)](), true},
+		{typeOf[func() foo](), true},
+		{typeOf[*foo](), true},
+		{typeOf[chan foo](), true},
+		{typeOf[[]foo](), true},
+		{typeOf[map[string]foo](), true},
+		// Unnamed type.
+		{typeOf[struct{}](), true},
+		{typeOf[func(struct{})](), true},
+		{typeOf[struct{ X int }](), true},
+	}
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
+			check := require.Error
+			if !tt.wantErr {
+				check = require.NoError
+			}
+
+			err := checkImportableType(tt.typ)
+			defer func() {
+				t.Logf("Kind: %q", tt.typ.Kind())
+				t.Logf("Package: %q", tt.typ.PkgPath())
+				t.Logf("Name: %q", tt.typ.Name())
+				if err != nil {
+					t.Logf("Error: %v", err)
+				}
+			}()
+
+			check(t, err)
+		})
+	}
+}
diff --git a/internal/integration/customformats/hextype/hex_test.go b/internal/integration/customformats/hextype/hex_test.go
new file mode 100644
index 00000000..a88b36c3
--- /dev/null
+++ b/internal/integration/customformats/hextype/hex_test.go
@@ -0,0 +1,50 @@
+package hextype
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/go-faster/jx"
+	"github.com/stretchr/testify/require"
+)
+
+func TestHex(t *testing.T) {
+	for i, tt := range []struct {
+		input   string
+		want    int64
+		wantErr bool
+	}{
+		{`"1"`, 1, false},
+		{`"a"`, 10, false},
+		{`"ff"`, 255, false},
+
+		// Wrong number format.
+		{`"-"`, 0, true},
+		{`"x"`, 0, true},
+		{`"fg"`, 0, true},
+		{`"0.0"`, 0, true},
+		// Wrong JSON type.
+		{`null`, 0, true},
+		{`true`, 0, true},
+		{`0`, 0, true},
+		// Invalid JSON.
+		{`"`, 0, true},
+	} {
+		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
+			a := require.New(t)
+
+			var enc JSONHexEncoding
+			v, err := enc.DecodeJSON(jx.DecodeStr(tt.input))
+			if tt.wantErr {
+				a.Error(err)
+				return
+			}
+			a.NoError(err)
+			a.Equal(tt.want, v)
+
+			e := jx.GetEncoder()
+			enc.EncodeJSON(e, tt.want)
+			a.JSONEq(tt.input, e.String())
+		})
+	}
+}
diff --git a/internal/integration/customformats_test.go b/internal/integration/customformats_test.go
index 1bd03fd1..934bdcfe 100644
--- a/internal/integration/customformats_test.go
+++ b/internal/integration/customformats_test.go
@@ -15,12 +15,14 @@ import (
 type testCustomFormats struct{}
 
 func (t testCustomFormats) PhoneGet(ctx context.Context, req *api.User, params api.PhoneGetParams) (*api.User, error) {
-	cpy := *req
-	cpy.HomePhone.SetTo(params.Phone)
+	req.HomePhone.SetTo(params.Phone)
 	if v, ok := params.Color.Get(); ok {
-		cpy.BackgroundColor.SetTo(v)
+		req.BackgroundColor.SetTo(v)
 	}
-	return &cpy, nil
+	if v, ok := params.Hex.Get(); ok {
+		req.HexColor.SetTo(v)
+	}
+	return req, nil
 }
 
 func TestCustomFormats(t *testing.T) {
@@ -39,7 +41,9 @@ func TestCustomFormats(t *testing.T) {
 	var (
 		homePhone       = phonetype.Phone("+1234567890")
 		backgroundColor = rgbatype.RGBA{R: 255, G: 0, B: 0, A: 255}
-		u               = &api.User{
+		hex             = int64(100)
+
+		u = &api.User{
 			ID:           10,
 			Phone:        "+1234567890",
 			ProfileColor: rgbatype.RGBA{R: 0, G: 0, B: 0, A: 255},
@@ -49,6 +53,7 @@ func TestCustomFormats(t *testing.T) {
 	u2, err := client.PhoneGet(ctx, u, api.PhoneGetParams{
 		Phone: homePhone,
 		Color: api.NewOptRgba(backgroundColor),
+		Hex:   api.NewOptHex(hex),
 	})
 	a.NoError(err)
 
@@ -57,4 +62,5 @@ func TestCustomFormats(t *testing.T) {
 	a.Equal(u.ProfileColor, u2.ProfileColor)
 	a.Equal(homePhone, u2.HomePhone.Or(""))
 	a.Equal(backgroundColor, u2.BackgroundColor.Or(rgbatype.RGBA{}))
+	a.Equal(hex, u2.HexColor.Or(0))
 }
diff --git a/internal/integration/test_customformats/oas_cfg_gen.go b/internal/integration/test_customformats/oas_cfg_gen.go
index f6a4159c..6eeb8257 100644
--- a/internal/integration/test_customformats/oas_cfg_gen.go
+++ b/internal/integration/test_customformats/oas_cfg_gen.go
@@ -11,23 +11,30 @@ import (
 	"go.opentelemetry.io/otel/trace"
 
 	ht "github.com/ogen-go/ogen/http"
-	custom0 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
-	custom1 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
+	custom0 "github.com/ogen-go/ogen/internal/integration/customformats/hextype"
+	custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
+	custom2 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
 	"github.com/ogen-go/ogen/middleware"
 	"github.com/ogen-go/ogen/ogenerrors"
 	"github.com/ogen-go/ogen/otelogen"
 )
 
 var (
+	formatHex = func() (r struct {
+		custom0.JSONHexEncoding
+		custom0.TextHexEncoding
+	}) {
+		return r
+	}
 	formatPhone = func() (r struct {
-		custom0.JSONPhoneEncoding
-		custom0.TextPhoneEncoding
+		custom1.JSONPhoneEncoding
+		custom1.TextPhoneEncoding
 	}) {
 		return r
 	}
 	formatRgba = func() (r struct {
-		custom1.JSONRGBAEncoding
-		custom1.TextRGBAEncoding
+		custom2.JSONRGBAEncoding
+		custom2.TextRGBAEncoding
 	}) {
 		return r
 	}
diff --git a/internal/integration/test_customformats/oas_client_gen.go b/internal/integration/test_customformats/oas_client_gen.go
index a9251c20..bef2db1c 100644
--- a/internal/integration/test_customformats/oas_client_gen.go
+++ b/internal/integration/test_customformats/oas_client_gen.go
@@ -130,6 +130,23 @@ func (c *Client) sendPhoneGet(ctx context.Context, request *User, params PhoneGe
 			return res, errors.Wrap(err, "encode query")
 		}
 	}
+	{
+		// Encode "hex" parameter.
+		cfg := uri.QueryParameterEncodingConfig{
+			Name:    "hex",
+			Style:   uri.QueryStyleForm,
+			Explode: true,
+		}
+
+		if err := q.EncodeParam(cfg, func(e uri.Encoder) error {
+			if val, ok := params.Hex.Get(); ok {
+				return e.EncodeValue(formatHex().EncodeText(val))
+			}
+			return nil
+		}); err != nil {
+			return res, errors.Wrap(err, "encode query")
+		}
+	}
 	u.RawQuery = q.Values().Encode()
 
 	stage = "EncodeRequest"
diff --git a/internal/integration/test_customformats/oas_handlers_gen.go b/internal/integration/test_customformats/oas_handlers_gen.go
index 607aad1c..5d11f2d2 100644
--- a/internal/integration/test_customformats/oas_handlers_gen.go
+++ b/internal/integration/test_customformats/oas_handlers_gen.go
@@ -90,6 +90,10 @@ func (s *Server) handlePhoneGetRequest(args [0]string, w http.ResponseWriter, r
 					Name: "color",
 					In:   "query",
 				}: params.Color,
+				{
+					Name: "hex",
+					In:   "query",
+				}: params.Hex,
 			},
 			Raw: r,
 		}
diff --git a/internal/integration/test_customformats/oas_json_gen.go b/internal/integration/test_customformats/oas_json_gen.go
index b1c08f70..b899e812 100644
--- a/internal/integration/test_customformats/oas_json_gen.go
+++ b/internal/integration/test_customformats/oas_json_gen.go
@@ -9,21 +9,56 @@ import (
 	"github.com/go-faster/errors"
 	"github.com/go-faster/jx"
 
-	custom0 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
-	custom1 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
+	custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
+	custom2 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
 	"github.com/ogen-go/ogen/validate"
 )
 
-// Encode encodes custom0.Phone as json.
-func (o OptPhone) Encode(e *jx.Encoder, format func(*jx.Encoder, custom0.Phone)) {
+// Encode encodes int64 as json.
+func (o OptHex) Encode(e *jx.Encoder, format func(*jx.Encoder, int64)) {
+	if !o.Set {
+		return
+	}
+	formatHex().EncodeJSON(e, o.Value)
+}
+
+// Decode decodes int64 from json.
+func (o *OptHex) Decode(d *jx.Decoder, format func(*jx.Decoder) (int64, error)) error {
+	if o == nil {
+		return errors.New("invalid: unable to decode OptHex to nil")
+	}
+	o.Set = true
+	v, err := formatHex().DecodeJSON(d)
+	if err != nil {
+		return err
+	}
+	o.Value = v
+	return nil
+}
+
+// MarshalJSON implements stdjson.Marshaler.
+func (s OptHex) MarshalJSON() ([]byte, error) {
+	e := jx.Encoder{}
+	s.Encode(&e, formatHex().EncodeJSON)
+	return e.Bytes(), nil
+}
+
+// UnmarshalJSON implements stdjson.Unmarshaler.
+func (s *OptHex) UnmarshalJSON(data []byte) error {
+	d := jx.DecodeBytes(data)
+	return s.Decode(d, formatHex().DecodeJSON)
+}
+
+// Encode encodes custom1.Phone as json.
+func (o OptPhone) Encode(e *jx.Encoder, format func(*jx.Encoder, custom1.Phone)) {
 	if !o.Set {
 		return
 	}
 	formatPhone().EncodeJSON(e, o.Value)
 }
 
-// Decode decodes custom0.Phone from json.
-func (o *OptPhone) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom0.Phone, error)) error {
+// Decode decodes custom1.Phone from json.
+func (o *OptPhone) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom1.Phone, error)) error {
 	if o == nil {
 		return errors.New("invalid: unable to decode OptPhone to nil")
 	}
@@ -49,16 +84,16 @@ func (s *OptPhone) UnmarshalJSON(data []byte) error {
 	return s.Decode(d, formatPhone().DecodeJSON)
 }
 
-// Encode encodes custom1.RGBA as json.
-func (o OptRgba) Encode(e *jx.Encoder, format func(*jx.Encoder, custom1.RGBA)) {
+// Encode encodes custom2.RGBA as json.
+func (o OptRgba) Encode(e *jx.Encoder, format func(*jx.Encoder, custom2.RGBA)) {
 	if !o.Set {
 		return
 	}
 	formatRgba().EncodeJSON(e, o.Value)
 }
 
-// Decode decodes custom1.RGBA from json.
-func (o *OptRgba) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom1.RGBA, error)) error {
+// Decode decodes custom2.RGBA from json.
+func (o *OptRgba) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom2.RGBA, error)) error {
 	if o == nil {
 		return errors.New("invalid: unable to decode OptRgba to nil")
 	}
@@ -120,14 +155,21 @@ func (s *User) encodeFields(e *jx.Encoder) {
 			s.BackgroundColor.Encode(e, formatRgba().EncodeJSON)
 		}
 	}
+	{
+		if s.HexColor.Set {
+			e.FieldStart("hex_color")
+			s.HexColor.Encode(e, formatHex().EncodeJSON)
+		}
+	}
 }
 
-var jsonFieldsNameOfUser = [5]string{
+var jsonFieldsNameOfUser = [6]string{
 	0: "id",
 	1: "phone",
 	2: "home_phone",
 	3: "profile_color",
 	4: "background_color",
+	5: "hex_color",
 }
 
 // Decode decodes User from json.
@@ -195,6 +237,16 @@ func (s *User) Decode(d *jx.Decoder) error {
 			}(); err != nil {
 				return errors.Wrap(err, "decode field \"background_color\"")
 			}
+		case "hex_color":
+			if err := func() error {
+				s.HexColor.Reset()
+				if err := s.HexColor.Decode(d, formatHex().DecodeJSON); err != nil {
+					return err
+				}
+				return nil
+			}(); err != nil {
+				return errors.Wrap(err, "decode field \"hex_color\"")
+			}
 		default:
 			return d.Skip()
 		}
diff --git a/internal/integration/test_customformats/oas_parameters_gen.go b/internal/integration/test_customformats/oas_parameters_gen.go
index 48f950db..f6443d51 100644
--- a/internal/integration/test_customformats/oas_parameters_gen.go
+++ b/internal/integration/test_customformats/oas_parameters_gen.go
@@ -5,8 +5,8 @@ package api
 import (
 	"net/http"
 
-	custom0 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
-	custom1 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
+	custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
+	custom2 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
 	"github.com/ogen-go/ogen/middleware"
 	"github.com/ogen-go/ogen/ogenerrors"
 	"github.com/ogen-go/ogen/uri"
@@ -16,9 +16,11 @@ import (
 // PhoneGetParams is parameters of GET /phone operation.
 type PhoneGetParams struct {
 	// Phone number.
-	Phone custom0.Phone
+	Phone custom1.Phone
 	// Color.
 	Color OptRgba
+	// Hex.
+	Hex OptHex
 }
 
 func unpackPhoneGetParams(packed middleware.Parameters) (params PhoneGetParams) {
@@ -27,7 +29,7 @@ func unpackPhoneGetParams(packed middleware.Parameters) (params PhoneGetParams)
 			Name: "phone",
 			In:   "query",
 		}
-		params.Phone = packed[key].(custom0.Phone)
+		params.Phone = packed[key].(custom1.Phone)
 	}
 	{
 		key := middleware.ParameterKey{
@@ -38,6 +40,15 @@ func unpackPhoneGetParams(packed middleware.Parameters) (params PhoneGetParams)
 			params.Color = v.(OptRgba)
 		}
 	}
+	{
+		key := middleware.ParameterKey{
+			Name: "hex",
+			In:   "query",
+		}
+		if v, ok := packed[key]; ok {
+			params.Hex = v.(OptHex)
+		}
+	}
 	return params
 }
 
@@ -89,7 +100,7 @@ func decodePhoneGetParams(args [0]string, r *http.Request) (params PhoneGetParam
 
 		if err := q.HasParam(cfg); err == nil {
 			if err := q.DecodeParam(cfg, func(d uri.Decoder) error {
-				var paramsDotColorVal custom1.RGBA
+				var paramsDotColorVal custom2.RGBA
 				if err := func() error {
 					val, err := d.DecodeValue()
 					if err != nil {
@@ -120,5 +131,46 @@ func decodePhoneGetParams(args [0]string, r *http.Request) (params PhoneGetParam
 			Err:  err,
 		}
 	}
+	// Decode query: hex.
+	if err := func() error {
+		cfg := uri.QueryParameterDecodingConfig{
+			Name:    "hex",
+			Style:   uri.QueryStyleForm,
+			Explode: true,
+		}
+
+		if err := q.HasParam(cfg); err == nil {
+			if err := q.DecodeParam(cfg, func(d uri.Decoder) error {
+				var paramsDotHexVal int64
+				if err := func() error {
+					val, err := d.DecodeValue()
+					if err != nil {
+						return err
+					}
+
+					c, err := formatHex().DecodeText(val)
+					if err != nil {
+						return err
+					}
+
+					paramsDotHexVal = c
+					return nil
+				}(); err != nil {
+					return err
+				}
+				params.Hex.SetTo(paramsDotHexVal)
+				return nil
+			}); err != nil {
+				return err
+			}
+		}
+		return nil
+	}(); err != nil {
+		return params, &ogenerrors.DecodeParamError{
+			Name: "hex",
+			In:   "query",
+			Err:  err,
+		}
+	}
 	return params, nil
 }
diff --git a/internal/integration/test_customformats/oas_schemas_gen.go b/internal/integration/test_customformats/oas_schemas_gen.go
index eaa83a92..1dc9f68c 100644
--- a/internal/integration/test_customformats/oas_schemas_gen.go
+++ b/internal/integration/test_customformats/oas_schemas_gen.go
@@ -3,21 +3,67 @@
 package api
 
 import (
-	custom0 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
-	custom1 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
+	custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
+	custom2 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
 )
 
+// NewOptHex returns new OptHex with value set to v.
+func NewOptHex(v int64) OptHex {
+	return OptHex{
+		Value: v,
+		Set:   true,
+	}
+}
+
+// OptHex is optional int64.
+type OptHex struct {
+	Value int64
+	Set   bool
+}
+
+// IsSet returns true if OptHex was set.
+func (o OptHex) IsSet() bool { return o.Set }
+
+// Reset unsets value.
+func (o *OptHex) Reset() {
+	var v int64
+	o.Value = v
+	o.Set = false
+}
+
+// SetTo sets value to v.
+func (o *OptHex) SetTo(v int64) {
+	o.Set = true
+	o.Value = v
+}
+
+// Get returns value and boolean that denotes whether value was set.
+func (o OptHex) Get() (v int64, ok bool) {
+	if !o.Set {
+		return v, false
+	}
+	return o.Value, true
+}
+
+// Or returns value if set, or given parameter if does not.
+func (o OptHex) Or(d int64) int64 {
+	if v, ok := o.Get(); ok {
+		return v
+	}
+	return d
+}
+
 // NewOptPhone returns new OptPhone with value set to v.
-func NewOptPhone(v custom0.Phone) OptPhone {
+func NewOptPhone(v custom1.Phone) OptPhone {
 	return OptPhone{
 		Value: v,
 		Set:   true,
 	}
 }
 
-// OptPhone is optional custom0.Phone.
+// OptPhone is optional custom1.Phone.
 type OptPhone struct {
-	Value custom0.Phone
+	Value custom1.Phone
 	Set   bool
 }
 
@@ -26,19 +72,19 @@ func (o OptPhone) IsSet() bool { return o.Set }
 
 // Reset unsets value.
 func (o *OptPhone) Reset() {
-	var v custom0.Phone
+	var v custom1.Phone
 	o.Value = v
 	o.Set = false
 }
 
 // SetTo sets value to v.
-func (o *OptPhone) SetTo(v custom0.Phone) {
+func (o *OptPhone) SetTo(v custom1.Phone) {
 	o.Set = true
 	o.Value = v
 }
 
 // Get returns value and boolean that denotes whether value was set.
-func (o OptPhone) Get() (v custom0.Phone, ok bool) {
+func (o OptPhone) Get() (v custom1.Phone, ok bool) {
 	if !o.Set {
 		return v, false
 	}
@@ -46,7 +92,7 @@ func (o OptPhone) Get() (v custom0.Phone, ok bool) {
 }
 
 // Or returns value if set, or given parameter if does not.
-func (o OptPhone) Or(d custom0.Phone) custom0.Phone {
+func (o OptPhone) Or(d custom1.Phone) custom1.Phone {
 	if v, ok := o.Get(); ok {
 		return v
 	}
@@ -54,16 +100,16 @@ func (o OptPhone) Or(d custom0.Phone) custom0.Phone {
 }
 
 // NewOptRgba returns new OptRgba with value set to v.
-func NewOptRgba(v custom1.RGBA) OptRgba {
+func NewOptRgba(v custom2.RGBA) OptRgba {
 	return OptRgba{
 		Value: v,
 		Set:   true,
 	}
 }
 
-// OptRgba is optional custom1.RGBA.
+// OptRgba is optional custom2.RGBA.
 type OptRgba struct {
-	Value custom1.RGBA
+	Value custom2.RGBA
 	Set   bool
 }
 
@@ -72,19 +118,19 @@ func (o OptRgba) IsSet() bool { return o.Set }
 
 // Reset unsets value.
 func (o *OptRgba) Reset() {
-	var v custom1.RGBA
+	var v custom2.RGBA
 	o.Value = v
 	o.Set = false
 }
 
 // SetTo sets value to v.
-func (o *OptRgba) SetTo(v custom1.RGBA) {
+func (o *OptRgba) SetTo(v custom2.RGBA) {
 	o.Set = true
 	o.Value = v
 }
 
 // Get returns value and boolean that denotes whether value was set.
-func (o OptRgba) Get() (v custom1.RGBA, ok bool) {
+func (o OptRgba) Get() (v custom2.RGBA, ok bool) {
 	if !o.Set {
 		return v, false
 	}
@@ -92,7 +138,7 @@ func (o OptRgba) Get() (v custom1.RGBA, ok bool) {
 }
 
 // Or returns value if set, or given parameter if does not.
-func (o OptRgba) Or(d custom1.RGBA) custom1.RGBA {
+func (o OptRgba) Or(d custom2.RGBA) custom2.RGBA {
 	if v, ok := o.Get(); ok {
 		return v
 	}
@@ -102,10 +148,11 @@ func (o OptRgba) Or(d custom1.RGBA) custom1.RGBA {
 // Ref: #/components/schemas/User
 type User struct {
 	ID              int64         `json:"id"`
-	Phone           custom0.Phone `json:"phone"`
+	Phone           custom1.Phone `json:"phone"`
 	HomePhone       OptPhone      `json:"home_phone"`
-	ProfileColor    custom1.RGBA  `json:"profile_color"`
+	ProfileColor    custom2.RGBA  `json:"profile_color"`
 	BackgroundColor OptRgba       `json:"background_color"`
+	HexColor        OptHex        `json:"hex_color"`
 }
 
 // GetID returns the value of ID.
@@ -114,7 +161,7 @@ func (s *User) GetID() int64 {
 }
 
 // GetPhone returns the value of Phone.
-func (s *User) GetPhone() custom0.Phone {
+func (s *User) GetPhone() custom1.Phone {
 	return s.Phone
 }
 
@@ -124,7 +171,7 @@ func (s *User) GetHomePhone() OptPhone {
 }
 
 // GetProfileColor returns the value of ProfileColor.
-func (s *User) GetProfileColor() custom1.RGBA {
+func (s *User) GetProfileColor() custom2.RGBA {
 	return s.ProfileColor
 }
 
@@ -133,13 +180,18 @@ func (s *User) GetBackgroundColor() OptRgba {
 	return s.BackgroundColor
 }
 
+// GetHexColor returns the value of HexColor.
+func (s *User) GetHexColor() OptHex {
+	return s.HexColor
+}
+
 // SetID sets the value of ID.
 func (s *User) SetID(val int64) {
 	s.ID = val
 }
 
 // SetPhone sets the value of Phone.
-func (s *User) SetPhone(val custom0.Phone) {
+func (s *User) SetPhone(val custom1.Phone) {
 	s.Phone = val
 }
 
@@ -149,7 +201,7 @@ func (s *User) SetHomePhone(val OptPhone) {
 }
 
 // SetProfileColor sets the value of ProfileColor.
-func (s *User) SetProfileColor(val custom1.RGBA) {
+func (s *User) SetProfileColor(val custom2.RGBA) {
 	s.ProfileColor = val
 }
 
@@ -157,3 +209,8 @@ func (s *User) SetProfileColor(val custom1.RGBA) {
 func (s *User) SetBackgroundColor(val OptRgba) {
 	s.BackgroundColor = val
 }
+
+// SetHexColor sets the value of HexColor.
+func (s *User) SetHexColor(val OptHex) {
+	s.HexColor = val
+}