Repository: ogen-go/ogen
Pull request #731 (2022-12-23 17:27:56)

feat(gen): allow to use empty interface as custom format type


Patch:
diff --git a/gen/custom_format.go b/gen/custom_format.go
index c4e83096..eb6415b3 100644
--- a/gen/custom_format.go
+++ b/gen/custom_format.go
@@ -19,7 +19,16 @@ func checkImportableType(typ reflect.Type) error {
 
 	name := typ.Name()
 	if name == "" {
-		return errors.New("type must be named or primitive")
+		switch typ.Kind() {
+		case reflect.Interface:
+			if typ.NumMethod() == 0 {
+				// Allow empty interface.
+				break
+			}
+			fallthrough
+		default:
+			return errors.New("type must be named or primitive")
+		}
 	}
 
 	if path != "" && !token.IsExported(name) {
diff --git a/gen/ir/custom_format.go b/gen/ir/custom_format.go
index 2af3d5d5..c680694c 100644
--- a/gen/ir/custom_format.go
+++ b/gen/ir/custom_format.go
@@ -15,8 +15,16 @@ 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()
+		switch t := c.Type; t.Kind() {
+		case reflect.Interface:
+			if t.NumMethod() != 0 {
+				panic(fmt.Sprintf("unexpected interface type: %v", t))
+			}
+			return "any"
+		default:
+			// Primitive type.
+			return t.Name()
+		}
 	}
 	return fmt.Sprintf("%s.%s", c.Pkg, c.Type.Name())
 }
diff --git a/gen/schema_gen.go b/gen/schema_gen.go
index ee51e6c5..af933580 100644
--- a/gen/schema_gen.go
+++ b/gen/schema_gen.go
@@ -276,6 +276,12 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T
 			return nil, errors.Wrap(err, "primitive")
 		}
 
+		fields := []zap.Field{
+			zapPosition(schema),
+			zap.String("type", string(schema.Type)),
+			zap.String("format", schema.Format),
+			zap.String("go_type", t.Go()),
+		}
 		switch schema.Type {
 		case jsonschema.String:
 			if err := t.Validators.SetString(schema); err != nil {
@@ -285,26 +291,39 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T
 				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()),
-					)
+					g.log.Warn("String validator cannot be applied to generated type and will be ignored", fields...)
 				}
 			}
 		case jsonschema.Integer:
 			if err := t.Validators.SetInt(schema); err != nil {
 				return nil, errors.Wrap(err, "int validator")
 			}
+			if t.Validators.Int.Set() {
+				switch t.Primitive {
+				case ir.String, ir.ByteSlice:
+				default:
+					g.log.Warn("Int validator cannot be applied to generated type and will be ignored", fields...)
+				}
+			}
 		case jsonschema.Number:
 			if err := t.Validators.SetFloat(schema); err != nil {
 				return nil, errors.Wrap(err, "float validator")
 			}
+			if t.Validators.Float.Set() {
+				switch t.Primitive {
+				case ir.String, ir.ByteSlice:
+				default:
+					g.log.Warn("Float validator cannot be applied to generated type and will be ignored", fields...)
+				}
+			}
 		}
 
 		return g.regtype(name, t), nil
 	case jsonschema.Empty:
+		if format, ok := g.customFormats[schema.Type][schema.Format]; ok {
+			return g.customFormat(format, schema), nil
+		}
+
 		g.log.Info("Type is not defined, using any",
 			zapPosition(schema),
 			zap.String("name", name),
diff --git a/gen/schema_gen_primitive.go b/gen/schema_gen_primitive.go
index 3c091bb1..f2295c01 100644
--- a/gen/schema_gen_primitive.go
+++ b/gen/schema_gen_primitive.go
@@ -153,9 +153,7 @@ func (g *schemaGen) parseSimple(schema *jsonschema.Schema) *ir.Type {
 	t, found := mapping[schema.Type][schema.Format]
 	if !found {
 		if custom, ok := g.customFormats[schema.Type][schema.Format]; ok {
-			typ := ir.Primitive(ir.Custom, schema)
-			typ.CustomFormat = &custom
-			return typ
+			return g.customFormat(custom, schema)
 		}
 		// Fallback to default.
 		t = mapping[schema.Type][""]
@@ -164,6 +162,12 @@ func (g *schemaGen) parseSimple(schema *jsonschema.Schema) *ir.Type {
 	return ir.Primitive(t, schema)
 }
 
+func (g *schemaGen) customFormat(custom ir.CustomFormat, schema *jsonschema.Schema) *ir.Type {
+	typ := ir.Primitive(ir.Custom, schema)
+	typ.CustomFormat = &custom
+	return typ
+}
+
 func TypeFormatMapping() map[jsonschema.SchemaType]map[string]ir.PrimitiveType {
 	return map[jsonschema.SchemaType]map[string]ir.PrimitiveType{
 		jsonschema.Integer: {
diff --git a/internal/integration/cmd/customformats/main.go b/internal/integration/cmd/customformats/main.go
index 117b50e8..1b74bad5 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/eventtype"
 	"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"
@@ -58,6 +59,9 @@ func run(specPath, targetDir string) error {
 				"rgba":  rgbatype.RGBAFormat,
 				"hex":   hextype.HexFormat,
 			},
+			jsonschema.Empty: {
+				"x-my-event": eventtype.EventFormat,
+			},
 		},
 		File:   location.NewFile(fileName, specPath, data),
 		Logger: l,
diff --git a/internal/integration/customformats/eventtype/eventtype.go b/internal/integration/customformats/eventtype/eventtype.go
new file mode 100644
index 00000000..27811bdc
--- /dev/null
+++ b/internal/integration/customformats/eventtype/eventtype.go
@@ -0,0 +1,60 @@
+// Package eventtype defines a custom format for event types.
+package eventtype
+
+import (
+	"encoding/json"
+
+	"github.com/go-faster/jx"
+
+	"github.com/ogen-go/ogen/gen"
+)
+
+type Event = any
+
+// EventFormat defines a custom format for Event.
+var EventFormat = gen.CustomFormat[
+	Event,
+	JSONEventEncoding,
+	TextEventEncoding,
+]()
+
+// JSONEventEncoding defines a custom JSON encoding for hexadecimal numbers.
+type JSONEventEncoding struct{}
+
+// EncodeJSON encodes a hexadecimal number as a JSON string.
+func (JSONEventEncoding) EncodeJSON(e *jx.Encoder, v Event) {
+	b, err := json.Marshal(v)
+	if err != nil {
+		e.Null()
+		return
+	}
+	e.Raw(b)
+}
+
+// DecodeJSON decodes a hexadecimal number from a JSON string.
+func (JSONEventEncoding) DecodeJSON(d *jx.Decoder) (v Event, _ error) {
+	r, err := d.Raw()
+	if err != nil {
+		return v, err
+	}
+	err = json.Unmarshal(r, &v)
+	return v, err
+}
+
+// TextEventEncoding defines a custom text encoding for hexadecimal numbers.
+type TextEventEncoding struct{}
+
+// EncodeText encodes a hexadecimal number as a string.
+func (TextEventEncoding) EncodeText(v Event) string {
+	b, err := json.Marshal(v)
+	if err != nil {
+		return ""
+	}
+	return string(b)
+}
+
+// DecodeText decodes a hexadecimal number from a string.
+func (TextEventEncoding) DecodeText(s string) (v Event, _ error) {
+	err := json.Unmarshal([]byte(s), &v)
+	return v, err
+}


Test patch:
diff --git a/_testdata/positive/custom_formats.json b/_testdata/positive/custom_formats.json
index cc8201e1..c1745d0e 100644
--- a/_testdata/positive/custom_formats.json
+++ b/_testdata/positive/custom_formats.json
@@ -5,6 +5,32 @@
     "version": "v0.1.0"
   },
   "paths": {
+    "/event": {
+      "post": {
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/Event"
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "description",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Event"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/phone": {
       "get": {
         "parameters": [
@@ -75,6 +101,9 @@
       }
     },
     "schemas": {
+      "Event": {
+        "format": "x-my-event"
+      },
       "User": {
         "type": "object",
         "required": [
diff --git a/internal/integration/customformats_test.go b/internal/integration/customformats_test.go
index 934bdcfe..229e483b 100644
--- a/internal/integration/customformats_test.go
+++ b/internal/integration/customformats_test.go
@@ -5,6 +5,7 @@ import (
 	"net/http/httptest"
 	"testing"
 
+	"github.com/go-faster/errors"
 	"github.com/stretchr/testify/require"
 
 	"github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
@@ -14,6 +15,13 @@ import (
 
 type testCustomFormats struct{}
 
+func (t testCustomFormats) EventPost(ctx context.Context, req any) (any, error) {
+	if req == nil {
+		return nil, errors.New("empty request")
+	}
+	return req, nil
+}
+
 func (t testCustomFormats) PhoneGet(ctx context.Context, req *api.User, params api.PhoneGetParams) (*api.User, error) {
 	req.HomePhone.SetTo(params.Phone)
 	if v, ok := params.Color.Get(); ok {
@@ -26,41 +34,61 @@ func (t testCustomFormats) PhoneGet(ctx context.Context, req *api.User, params a
 }
 
 func TestCustomFormats(t *testing.T) {
-	a := require.New(t)
 	ctx := context.Background()
 
 	srv, err := api.NewServer(testCustomFormats{})
-	a.NoError(err)
+	require.NoError(t, err)
 
 	s := httptest.NewServer(srv)
 	defer s.Close()
 
 	client, err := api.NewClient(s.URL, api.WithClient(s.Client()))
-	a.NoError(err)
+	require.NoError(t, err)
 
-	var (
-		homePhone       = phonetype.Phone("+1234567890")
-		backgroundColor = rgbatype.RGBA{R: 255, G: 0, B: 0, A: 255}
-		hex             = int64(100)
+	t.Run("EventPost", func(t *testing.T) {
+		a := require.New(t)
 
-		u = &api.User{
-			ID:           10,
-			Phone:        "+1234567890",
-			ProfileColor: rgbatype.RGBA{R: 0, G: 0, B: 0, A: 255},
+		for _, val := range []any{
+			true,
+			float64(42),
+			"string",
+			[]any{float64(1), float64(2), float64(3)},
+			map[string]any{
+				"key": []any{"value", "value2"},
+			},
+		} {
+			result, err := client.EventPost(ctx, val)
+			a.NoError(err)
+			a.Equal(val, result)
 		}
-	)
+	})
+	t.Run("Phone", func(t *testing.T) {
+		a := require.New(t)
+
+		var (
+			homePhone       = phonetype.Phone("+1234567890")
+			backgroundColor = rgbatype.RGBA{R: 255, G: 0, B: 0, A: 255}
+			hex             = int64(100)
+
+			u = &api.User{
+				ID:           10,
+				Phone:        "+1234567890",
+				ProfileColor: rgbatype.RGBA{R: 0, G: 0, B: 0, A: 255},
+			}
+		)
+
+		u2, err := client.PhoneGet(ctx, u, api.PhoneGetParams{
+			Phone: homePhone,
+			Color: api.NewOptRgba(backgroundColor),
+			Hex:   api.NewOptHex(hex),
+		})
+		a.NoError(err)
 
-	u2, err := client.PhoneGet(ctx, u, api.PhoneGetParams{
-		Phone: homePhone,
-		Color: api.NewOptRgba(backgroundColor),
-		Hex:   api.NewOptHex(hex),
+		a.Equal(u.ID, u2.ID)
+		a.Equal(u.Phone, u2.Phone)
+		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))
 	})
-	a.NoError(err)
-
-	a.Equal(u.ID, u2.ID)
-	a.Equal(u.Phone, u2.Phone)
-	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 6eeb8257..d6f6ca41 100644
--- a/internal/integration/test_customformats/oas_cfg_gen.go
+++ b/internal/integration/test_customformats/oas_cfg_gen.go
@@ -11,9 +11,10 @@ import (
 	"go.opentelemetry.io/otel/trace"
 
 	ht "github.com/ogen-go/ogen/http"
-	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"
+	custom0 "github.com/ogen-go/ogen/internal/integration/customformats/eventtype"
+	custom1 "github.com/ogen-go/ogen/internal/integration/customformats/hextype"
+	custom2 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
+	custom3 "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"
@@ -21,20 +22,26 @@ import (
 
 var (
 	formatHex = func() (r struct {
-		custom0.JSONHexEncoding
-		custom0.TextHexEncoding
+		custom1.JSONHexEncoding
+		custom1.TextHexEncoding
 	}) {
 		return r
 	}
 	formatPhone = func() (r struct {
-		custom1.JSONPhoneEncoding
-		custom1.TextPhoneEncoding
+		custom2.JSONPhoneEncoding
+		custom2.TextPhoneEncoding
 	}) {
 		return r
 	}
 	formatRgba = func() (r struct {
-		custom2.JSONRGBAEncoding
-		custom2.TextRGBAEncoding
+		custom3.JSONRGBAEncoding
+		custom3.TextRGBAEncoding
+	}) {
+		return r
+	}
+	formatXMyEvent = func() (r struct {
+		custom0.JSONEventEncoding
+		custom0.TextEventEncoding
 	}) {
 		return r
 	}
diff --git a/internal/integration/test_customformats/oas_client_gen.go b/internal/integration/test_customformats/oas_client_gen.go
index bef2db1c..8292a8fb 100644
--- a/internal/integration/test_customformats/oas_client_gen.go
+++ b/internal/integration/test_customformats/oas_client_gen.go
@@ -56,6 +56,72 @@ func (c *Client) requestURL(ctx context.Context) *url.URL {
 	return u
 }
 
+// EventPost invokes POST /event operation.
+//
+// POST /event
+func (c *Client) EventPost(ctx context.Context, request any) (any, error) {
+	res, err := c.sendEventPost(ctx, request)
+	_ = res
+	return res, err
+}
+
+func (c *Client) sendEventPost(ctx context.Context, request any) (res any, err error) {
+	var otelAttrs []attribute.KeyValue
+
+	// Run stopwatch.
+	startTime := time.Now()
+	defer func() {
+		elapsedDuration := time.Since(startTime)
+		c.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
+	}()
+
+	// Increment request counter.
+	c.requests.Add(ctx, 1, otelAttrs...)
+
+	// Start a span for this request.
+	ctx, span := c.cfg.Tracer.Start(ctx, "EventPost",
+		clientSpanKind,
+	)
+	// Track stage for error reporting.
+	var stage string
+	defer func() {
+		if err != nil {
+			span.RecordError(err)
+			span.SetStatus(codes.Error, stage)
+			c.errors.Add(ctx, 1, otelAttrs...)
+		}
+		span.End()
+	}()
+
+	stage = "BuildURL"
+	u := uri.Clone(c.requestURL(ctx))
+	u.Path += "/event"
+
+	stage = "EncodeRequest"
+	r, err := ht.NewRequest(ctx, "POST", u, nil)
+	if err != nil {
+		return res, errors.Wrap(err, "create request")
+	}
+	if err := encodeEventPostRequest(request, r); err != nil {
+		return res, errors.Wrap(err, "encode request")
+	}
+
+	stage = "SendRequest"
+	resp, err := c.cfg.Client.Do(r)
+	if err != nil {
+		return res, errors.Wrap(err, "do request")
+	}
+	defer resp.Body.Close()
+
+	stage = "DecodeResponse"
+	result, err := decodeEventPostResponse(resp)
+	if err != nil {
+		return res, errors.Wrap(err, "decode response")
+	}
+
+	return result, nil
+}
+
 // PhoneGet invokes GET /phone operation.
 //
 // GET /phone
diff --git a/internal/integration/test_customformats/oas_handlers_gen.go b/internal/integration/test_customformats/oas_handlers_gen.go
index 5d11f2d2..19c10bab 100644
--- a/internal/integration/test_customformats/oas_handlers_gen.go
+++ b/internal/integration/test_customformats/oas_handlers_gen.go
@@ -14,6 +14,101 @@ import (
 	"github.com/ogen-go/ogen/ogenerrors"
 )
 
+// handleEventPostRequest handles POST /event operation.
+//
+// POST /event
+func (s *Server) handleEventPostRequest(args [0]string, w http.ResponseWriter, r *http.Request) {
+	var otelAttrs []attribute.KeyValue
+
+	// Start a span for this request.
+	ctx, span := s.cfg.Tracer.Start(r.Context(), "EventPost",
+		serverSpanKind,
+	)
+	defer span.End()
+
+	// Run stopwatch.
+	startTime := time.Now()
+	defer func() {
+		elapsedDuration := time.Since(startTime)
+		s.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
+	}()
+
+	// Increment request counter.
+	s.requests.Add(ctx, 1, otelAttrs...)
+
+	var (
+		recordError = func(stage string, err error) {
+			span.RecordError(err)
+			span.SetStatus(codes.Error, stage)
+			s.errors.Add(ctx, 1, otelAttrs...)
+		}
+		err          error
+		opErrContext = ogenerrors.OperationContext{
+			Name: "EventPost",
+			ID:   "",
+		}
+	)
+	request, close, err := s.decodeEventPostRequest(r)
+	if err != nil {
+		err = &ogenerrors.DecodeRequestError{
+			OperationContext: opErrContext,
+			Err:              err,
+		}
+		recordError("DecodeRequest", err)
+		s.cfg.ErrorHandler(ctx, w, r, err)
+		return
+	}
+	defer func() {
+		if err := close(); err != nil {
+			recordError("CloseRequest", err)
+		}
+	}()
+
+	var response any
+	if m := s.cfg.Middleware; m != nil {
+		mreq := middleware.Request{
+			Context:       ctx,
+			OperationName: "EventPost",
+			OperationID:   "",
+			Body:          request,
+			Params:        middleware.Parameters{},
+			Raw:           r,
+		}
+
+		type (
+			Request  = any
+			Params   = struct{}
+			Response = any
+		)
+		response, err = middleware.HookMiddleware[
+			Request,
+			Params,
+			Response,
+		](
+			m,
+			mreq,
+			nil,
+			func(ctx context.Context, request Request, params Params) (response Response, err error) {
+				response, err = s.h.EventPost(ctx, request)
+				return response, err
+			},
+		)
+	} else {
+		response, err = s.h.EventPost(ctx, request)
+	}
+	if err != nil {
+		recordError("Internal", err)
+		s.cfg.ErrorHandler(ctx, w, r, err)
+		return
+	}
+
+	if err := encodeEventPostResponse(response, w, span); err != nil {
+		recordError("EncodeResponse", err)
+		s.cfg.ErrorHandler(ctx, w, r, err)
+		return
+	}
+}
+
 // handlePhoneGetRequest handles GET /phone operation.
 //
 // GET /phone
diff --git a/internal/integration/test_customformats/oas_json_gen.go b/internal/integration/test_customformats/oas_json_gen.go
index b899e812..aaa25427 100644
--- a/internal/integration/test_customformats/oas_json_gen.go
+++ b/internal/integration/test_customformats/oas_json_gen.go
@@ -9,8 +9,8 @@ import (
 	"github.com/go-faster/errors"
 	"github.com/go-faster/jx"
 
-	custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
-	custom2 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
+	custom2 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
+	custom3 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
 	"github.com/ogen-go/ogen/validate"
 )
 
@@ -49,16 +49,16 @@ func (s *OptHex) UnmarshalJSON(data []byte) error {
 	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)) {
+// Encode encodes custom2.Phone as json.
+func (o OptPhone) Encode(e *jx.Encoder, format func(*jx.Encoder, custom2.Phone)) {
 	if !o.Set {
 		return
 	}
 	formatPhone().EncodeJSON(e, o.Value)
 }
 
-// Decode decodes custom1.Phone from json.
-func (o *OptPhone) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom1.Phone, error)) error {
+// Decode decodes custom2.Phone from json.
+func (o *OptPhone) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom2.Phone, error)) error {
 	if o == nil {
 		return errors.New("invalid: unable to decode OptPhone to nil")
 	}
@@ -84,16 +84,16 @@ func (s *OptPhone) UnmarshalJSON(data []byte) error {
 	return s.Decode(d, formatPhone().DecodeJSON)
 }
 
-// Encode encodes custom2.RGBA as json.
-func (o OptRgba) Encode(e *jx.Encoder, format func(*jx.Encoder, custom2.RGBA)) {
+// Encode encodes custom3.RGBA as json.
+func (o OptRgba) Encode(e *jx.Encoder, format func(*jx.Encoder, custom3.RGBA)) {
 	if !o.Set {
 		return
 	}
 	formatRgba().EncodeJSON(e, o.Value)
 }
 
-// Decode decodes custom2.RGBA from json.
-func (o *OptRgba) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom2.RGBA, error)) error {
+// Decode decodes custom3.RGBA from json.
+func (o *OptRgba) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom3.RGBA, error)) error {
 	if o == nil {
 		return errors.New("invalid: unable to decode OptRgba to nil")
 	}
diff --git a/internal/integration/test_customformats/oas_parameters_gen.go b/internal/integration/test_customformats/oas_parameters_gen.go
index f6443d51..0722248d 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"
 
-	custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
-	custom2 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
+	custom2 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
+	custom3 "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,7 +16,7 @@ import (
 // PhoneGetParams is parameters of GET /phone operation.
 type PhoneGetParams struct {
 	// Phone number.
-	Phone custom1.Phone
+	Phone custom2.Phone
 	// Color.
 	Color OptRgba
 	// Hex.
@@ -29,7 +29,7 @@ func unpackPhoneGetParams(packed middleware.Parameters) (params PhoneGetParams)
 			Name: "phone",
 			In:   "query",
 		}
-		params.Phone = packed[key].(custom1.Phone)
+		params.Phone = packed[key].(custom2.Phone)
 	}
 	{
 		key := middleware.ParameterKey{
@@ -100,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 custom2.RGBA
+				var paramsDotColorVal custom3.RGBA
 				if err := func() error {
 					val, err := d.DecodeValue()
 					if err != nil {
diff --git a/internal/integration/test_customformats/oas_request_decoders_gen.go b/internal/integration/test_customformats/oas_request_decoders_gen.go
index fb414e57..335e185b 100644
--- a/internal/integration/test_customformats/oas_request_decoders_gen.go
+++ b/internal/integration/test_customformats/oas_request_decoders_gen.go
@@ -15,6 +15,71 @@ import (
 	"github.com/ogen-go/ogen/validate"
 )
 
+func (s *Server) decodeEventPostRequest(r *http.Request) (
+	req any,
+	close func() error,
+	rerr error,
+) {
+	var closers []func() error
+	close = func() error {
+		var merr error
+		// Close in reverse order, to match defer behavior.
+		for i := len(closers) - 1; i >= 0; i-- {
+			c := closers[i]
+			merr = multierr.Append(merr, c())
+		}
+		return merr
+	}
+	defer func() {
+		if rerr != nil {
+			rerr = multierr.Append(rerr, close())
+		}
+	}()
+	ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
+	if err != nil {
+		return req, close, errors.Wrap(err, "parse media type")
+	}
+	switch {
+	case ct == "application/json":
+		if r.ContentLength == 0 {
+			return req, close, validate.ErrBodyRequired
+		}
+
+		var request any
+		buf, err := io.ReadAll(r.Body)
+		if err != nil {
+			return req, close, err
+		}
+
+		if len(buf) == 0 {
+			return req, close, validate.ErrBodyRequired
+		}
+
+		d := jx.DecodeBytes(buf)
+		if err := func() error {
+			v, err := formatXMyEvent().DecodeJSON(d)
+			request = v
+			if err != nil {
+				return err
+			}
+			if err := d.Skip(); err != io.EOF {
+				return errors.New("unexpected trailing data")
+			}
+			return nil
+		}(); err != nil {
+			err = &ogenerrors.DecodeBodyError{
+				ContentType: ct,
+				Body:        buf,
+				Err:         err,
+			}
+			return req, close, err
+		}
+		return request, close, nil
+	default:
+		return req, close, validate.InvalidContentType(ct)
+	}
+}
+
 func (s *Server) decodePhoneGetRequest(r *http.Request) (
 	req *User,
 	close func() error,
diff --git a/internal/integration/test_customformats/oas_request_encoders_gen.go b/internal/integration/test_customformats/oas_request_encoders_gen.go
index 3cc38849..fb00aa19 100644
--- a/internal/integration/test_customformats/oas_request_encoders_gen.go
+++ b/internal/integration/test_customformats/oas_request_encoders_gen.go
@@ -11,6 +11,20 @@ import (
 	ht "github.com/ogen-go/ogen/http"
 )
 
+func encodeEventPostRequest(
+	req any,
+	r *http.Request,
+) error {
+	const contentType = "application/json"
+	e := jx.GetEncoder()
+	{
+		formatXMyEvent().EncodeJSON(e, req)
+	}
+	encoded := e.Bytes()
+	ht.SetBody(r, bytes.NewReader(encoded), contentType)
+	return nil
+}
+
 func encodePhoneGetRequest(
 	req *User,
 	r *http.Request,
diff --git a/internal/integration/test_customformats/oas_response_decoders_gen.go b/internal/integration/test_customformats/oas_response_decoders_gen.go
index da3613dc..874ffadf 100644
--- a/internal/integration/test_customformats/oas_response_decoders_gen.go
+++ b/internal/integration/test_customformats/oas_response_decoders_gen.go
@@ -14,6 +14,49 @@ import (
 	"github.com/ogen-go/ogen/validate"
 )
 
+func decodeEventPostResponse(resp *http.Response) (res any, err error) {
+	switch resp.StatusCode {
+	case 200:
+		// Code 200.
+		ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
+		if err != nil {
+			return res, errors.Wrap(err, "parse media type")
+		}
+		switch {
+		case ct == "application/json":
+			buf, err := io.ReadAll(resp.Body)
+			if err != nil {
+				return res, err
+			}
+
+			d := jx.DecodeBytes(buf)
+			var response any
+			if err := func() error {
+				v, err := formatXMyEvent().DecodeJSON(d)
+				response = v
+				if err != nil {
+					return err
+				}
+				if err := d.Skip(); err != io.EOF {
+					return errors.New("unexpected trailing data")
+				}
+				return nil
+			}(); err != nil {
+				err = &ogenerrors.DecodeBodyError{
+					ContentType: ct,
+					Body:        buf,
+					Err:         err,
+				}
+				return res, err
+			}
+			return response, nil
+		default:
+			return res, validate.InvalidContentType(ct)
+		}
+	}
+	return res, validate.UnexpectedStatusCode(resp.StatusCode)
+}
+
 func decodePhoneGetResponse(resp *http.Response) (res *User, err error) {
 	switch resp.StatusCode {
 	case 200:
diff --git a/internal/integration/test_customformats/oas_response_encoders_gen.go b/internal/integration/test_customformats/oas_response_encoders_gen.go
index a3aafa72..17ec5c7a 100644
--- a/internal/integration/test_customformats/oas_response_encoders_gen.go
+++ b/internal/integration/test_customformats/oas_response_encoders_gen.go
@@ -11,6 +11,19 @@ import (
 	"go.opentelemetry.io/otel/trace"
 )
 
+func encodeEventPostResponse(response any, w http.ResponseWriter, span trace.Span) error {
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(200)
+	span.SetStatus(codes.Ok, http.StatusText(200))
+
+	e := jx.GetEncoder()
+	formatXMyEvent().EncodeJSON(e, response)
+	if _, err := e.WriteTo(w); err != nil {
+		return errors.Wrap(err, "write")
+	}
+	return nil
+}
+
 func encodePhoneGetResponse(response *User, w http.ResponseWriter, span trace.Span) error {
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(200)
diff --git a/internal/integration/test_customformats/oas_router_gen.go b/internal/integration/test_customformats/oas_router_gen.go
index 8109c363..0b6bc247 100644
--- a/internal/integration/test_customformats/oas_router_gen.go
+++ b/internal/integration/test_customformats/oas_router_gen.go
@@ -41,23 +41,53 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			break
 		}
 		switch elem[0] {
-		case '/': // Prefix: "/phone"
-			if l := len("/phone"); len(elem) >= l && elem[0:l] == "/phone" {
+		case '/': // Prefix: "/"
+			if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
 				elem = elem[l:]
 			} else {
 				break
 			}
 
 			if len(elem) == 0 {
-				// Leaf node.
-				switch r.Method {
-				case "GET":
-					s.handlePhoneGetRequest([0]string{}, w, r)
-				default:
-					s.notAllowed(w, r, "GET")
+				break
+			}
+			switch elem[0] {
+			case 'e': // Prefix: "event"
+				if l := len("event"); len(elem) >= l && elem[0:l] == "event" {
+					elem = elem[l:]
+				} else {
+					break
 				}
 
-				return
+				if len(elem) == 0 {
+					// Leaf node.
+					switch r.Method {
+					case "POST":
+						s.handleEventPostRequest([0]string{}, w, r)
+					default:
+						s.notAllowed(w, r, "POST")
+					}
+
+					return
+				}
+			case 'p': // Prefix: "phone"
+				if l := len("phone"); len(elem) >= l && elem[0:l] == "phone" {
+					elem = elem[l:]
+				} else {
+					break
+				}
+
+				if len(elem) == 0 {
+					// Leaf node.
+					switch r.Method {
+					case "GET":
+						s.handlePhoneGetRequest([0]string{}, w, r)
+					default:
+						s.notAllowed(w, r, "GET")
+					}
+
+					return
+				}
 			}
 		}
 	}
@@ -122,24 +152,56 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) {
 			break
 		}
 		switch elem[0] {
-		case '/': // Prefix: "/phone"
-			if l := len("/phone"); len(elem) >= l && elem[0:l] == "/phone" {
+		case '/': // Prefix: "/"
+			if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
 				elem = elem[l:]
 			} else {
 				break
 			}
 
 			if len(elem) == 0 {
-				switch method {
-				case "GET":
-					// Leaf: PhoneGet
-					r.name = "PhoneGet"
-					r.operationID = ""
-					r.args = args
-					r.count = 0
-					return r, true
-				default:
-					return
+				break
+			}
+			switch elem[0] {
+			case 'e': // Prefix: "event"
+				if l := len("event"); len(elem) >= l && elem[0:l] == "event" {
+					elem = elem[l:]
+				} else {
+					break
+				}
+
+				if len(elem) == 0 {
+					switch method {
+					case "POST":
+						// Leaf: EventPost
+						r.name = "EventPost"
+						r.operationID = ""
+						r.args = args
+						r.count = 0
+						return r, true
+					default:
+						return
+					}
+				}
+			case 'p': // Prefix: "phone"
+				if l := len("phone"); len(elem) >= l && elem[0:l] == "phone" {
+					elem = elem[l:]
+				} else {
+					break
+				}
+
+				if len(elem) == 0 {
+					switch method {
+					case "GET":
+						// Leaf: PhoneGet
+						r.name = "PhoneGet"
+						r.operationID = ""
+						r.args = args
+						r.count = 0
+						return r, true
+					default:
+						return
+					}
 				}
 			}
 		}
diff --git a/internal/integration/test_customformats/oas_schemas_gen.go b/internal/integration/test_customformats/oas_schemas_gen.go
index 1dc9f68c..265f8f75 100644
--- a/internal/integration/test_customformats/oas_schemas_gen.go
+++ b/internal/integration/test_customformats/oas_schemas_gen.go
@@ -3,8 +3,8 @@
 package api
 
 import (
-	custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
-	custom2 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
+	custom2 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
+	custom3 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
 )
 
 // NewOptHex returns new OptHex with value set to v.
@@ -54,16 +54,16 @@ func (o OptHex) Or(d int64) int64 {
 }
 
 // NewOptPhone returns new OptPhone with value set to v.
-func NewOptPhone(v custom1.Phone) OptPhone {
+func NewOptPhone(v custom2.Phone) OptPhone {
 	return OptPhone{
 		Value: v,
 		Set:   true,
 	}
 }
 
-// OptPhone is optional custom1.Phone.
+// OptPhone is optional custom2.Phone.
 type OptPhone struct {
-	Value custom1.Phone
+	Value custom2.Phone
 	Set   bool
 }
 
@@ -72,19 +72,19 @@ func (o OptPhone) IsSet() bool { return o.Set }
 
 // Reset unsets value.
 func (o *OptPhone) Reset() {
-	var v custom1.Phone
+	var v custom2.Phone
 	o.Value = v
 	o.Set = false
 }
 
 // SetTo sets value to v.
-func (o *OptPhone) SetTo(v custom1.Phone) {
+func (o *OptPhone) SetTo(v custom2.Phone) {
 	o.Set = true
 	o.Value = v
 }
 
 // Get returns value and boolean that denotes whether value was set.
-func (o OptPhone) Get() (v custom1.Phone, ok bool) {
+func (o OptPhone) Get() (v custom2.Phone, ok bool) {
 	if !o.Set {
 		return v, false
 	}
@@ -92,7 +92,7 @@ func (o OptPhone) Get() (v custom1.Phone, ok bool) {
 }
 
 // Or returns value if set, or given parameter if does not.
-func (o OptPhone) Or(d custom1.Phone) custom1.Phone {
+func (o OptPhone) Or(d custom2.Phone) custom2.Phone {
 	if v, ok := o.Get(); ok {
 		return v
 	}
@@ -100,16 +100,16 @@ func (o OptPhone) Or(d custom1.Phone) custom1.Phone {
 }
 
 // NewOptRgba returns new OptRgba with value set to v.
-func NewOptRgba(v custom2.RGBA) OptRgba {
+func NewOptRgba(v custom3.RGBA) OptRgba {
 	return OptRgba{
 		Value: v,
 		Set:   true,
 	}
 }
 
-// OptRgba is optional custom2.RGBA.
+// OptRgba is optional custom3.RGBA.
 type OptRgba struct {
-	Value custom2.RGBA
+	Value custom3.RGBA
 	Set   bool
 }
 
@@ -118,19 +118,19 @@ func (o OptRgba) IsSet() bool { return o.Set }
 
 // Reset unsets value.
 func (o *OptRgba) Reset() {
-	var v custom2.RGBA
+	var v custom3.RGBA
 	o.Value = v
 	o.Set = false
 }
 
 // SetTo sets value to v.
-func (o *OptRgba) SetTo(v custom2.RGBA) {
+func (o *OptRgba) SetTo(v custom3.RGBA) {
 	o.Set = true
 	o.Value = v
 }
 
 // Get returns value and boolean that denotes whether value was set.
-func (o OptRgba) Get() (v custom2.RGBA, ok bool) {
+func (o OptRgba) Get() (v custom3.RGBA, ok bool) {
 	if !o.Set {
 		return v, false
 	}
@@ -138,7 +138,7 @@ func (o OptRgba) Get() (v custom2.RGBA, ok bool) {
 }
 
 // Or returns value if set, or given parameter if does not.
-func (o OptRgba) Or(d custom2.RGBA) custom2.RGBA {
+func (o OptRgba) Or(d custom3.RGBA) custom3.RGBA {
 	if v, ok := o.Get(); ok {
 		return v
 	}
@@ -148,9 +148,9 @@ func (o OptRgba) Or(d custom2.RGBA) custom2.RGBA {
 // Ref: #/components/schemas/User
 type User struct {
 	ID              int64         `json:"id"`
-	Phone           custom1.Phone `json:"phone"`
+	Phone           custom2.Phone `json:"phone"`
 	HomePhone       OptPhone      `json:"home_phone"`
-	ProfileColor    custom2.RGBA  `json:"profile_color"`
+	ProfileColor    custom3.RGBA  `json:"profile_color"`
 	BackgroundColor OptRgba       `json:"background_color"`
 	HexColor        OptHex        `json:"hex_color"`
 }
@@ -161,7 +161,7 @@ func (s *User) GetID() int64 {
 }
 
 // GetPhone returns the value of Phone.
-func (s *User) GetPhone() custom1.Phone {
+func (s *User) GetPhone() custom2.Phone {
 	return s.Phone
 }
 
@@ -171,7 +171,7 @@ func (s *User) GetHomePhone() OptPhone {
 }
 
 // GetProfileColor returns the value of ProfileColor.
-func (s *User) GetProfileColor() custom2.RGBA {
+func (s *User) GetProfileColor() custom3.RGBA {
 	return s.ProfileColor
 }
 
@@ -191,7 +191,7 @@ func (s *User) SetID(val int64) {
 }
 
 // SetPhone sets the value of Phone.
-func (s *User) SetPhone(val custom1.Phone) {
+func (s *User) SetPhone(val custom2.Phone) {
 	s.Phone = val
 }
 
@@ -201,7 +201,7 @@ func (s *User) SetHomePhone(val OptPhone) {
 }
 
 // SetProfileColor sets the value of ProfileColor.
-func (s *User) SetProfileColor(val custom2.RGBA) {
+func (s *User) SetProfileColor(val custom3.RGBA) {
 	s.ProfileColor = val
 }
 
diff --git a/internal/integration/test_customformats/oas_server_gen.go b/internal/integration/test_customformats/oas_server_gen.go
index a79255db..c78d04bc 100644
--- a/internal/integration/test_customformats/oas_server_gen.go
+++ b/internal/integration/test_customformats/oas_server_gen.go
@@ -8,6 +8,10 @@ import (
 
 // Handler handles operations described by OpenAPI v3 specification.
 type Handler interface {
+	// EventPost implements POST /event operation.
+	//
+	// POST /event
+	EventPost(ctx context.Context, req any) (any, error)
 	// PhoneGet implements GET /phone operation.
 	//
 	// GET /phone
diff --git a/internal/integration/test_customformats/oas_unimplemented_gen.go b/internal/integration/test_customformats/oas_unimplemented_gen.go
index f447223b..0c5d9b0a 100644
--- a/internal/integration/test_customformats/oas_unimplemented_gen.go
+++ b/internal/integration/test_customformats/oas_unimplemented_gen.go
@@ -13,6 +13,13 @@ type UnimplementedHandler struct{}
 
 var _ Handler = UnimplementedHandler{}
 
+// EventPost implements POST /event operation.
+//
+// POST /event
+func (UnimplementedHandler) EventPost(ctx context.Context, req any) (r any, _ error) {
+	return r, ht.ErrNotImplemented
+}
+
 // PhoneGet implements GET /phone operation.
 //
 // GET /phone