Repository: ogen-go/ogen
Pull request #357 (2022-05-23 15:17:05)

fix(jsonschema): pass format to untyped schema


Patch:
diff --git a/examples/ex_manga/oas_client_gen.go b/examples/ex_manga/oas_client_gen.go
index fd5c195e..3bfcc87a 100644
--- a/examples/ex_manga/oas_client_gen.go
+++ b/examples/ex_manga/oas_client_gen.go
@@ -106,6 +106,252 @@ func (c *Client) GetBook(ctx context.Context, params GetBookParams) (res GetBook
 	return result, nil
 }
 
+// GetPageCoverImage invokes getPageCoverImage operation.
+//
+// GET /galleries/{media_id}/cover.{format}
+func (c *Client) GetPageCoverImage(ctx context.Context, params GetPageCoverImageParams) (res GetPageCoverImageRes, err error) {
+	startTime := time.Now()
+	otelAttrs := []attribute.KeyValue{
+		otelogen.OperationID("getPageCoverImage"),
+	}
+	ctx, span := c.cfg.Tracer.Start(ctx, "GetPageCoverImage",
+		trace.WithAttributes(otelAttrs...),
+		trace.WithSpanKind(trace.SpanKindClient),
+	)
+	defer func() {
+		if err != nil {
+			span.RecordError(err)
+			c.errors.Add(ctx, 1, otelAttrs...)
+		} else {
+			elapsedDuration := time.Since(startTime)
+			c.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
+		}
+		span.End()
+	}()
+	c.requests.Add(ctx, 1, otelAttrs...)
+	u := uri.Clone(c.serverURL)
+	u.Path += "/galleries/"
+	{
+		// Encode "media_id" parameter.
+		e := uri.NewPathEncoder(uri.PathEncoderConfig{
+			Param:   "media_id",
+			Style:   uri.PathStyleSimple,
+			Explode: false,
+		})
+		if err := func() error {
+			return e.EncodeValue(conv.IntToString(params.MediaID))
+		}(); err != nil {
+			return res, errors.Wrap(err, "encode path")
+		}
+		u.Path += e.Result()
+	}
+	u.Path += "/cover."
+	{
+		// Encode "format" parameter.
+		e := uri.NewPathEncoder(uri.PathEncoderConfig{
+			Param:   "format",
+			Style:   uri.PathStyleSimple,
+			Explode: false,
+		})
+		if err := func() error {
+			return e.EncodeValue(conv.StringToString(params.Format))
+		}(); err != nil {
+			return res, errors.Wrap(err, "encode path")
+		}
+		u.Path += e.Result()
+	}
+
+	r := ht.NewRequest(ctx, "GET", u, nil)
+	defer ht.PutRequest(r)
+
+	resp, err := c.cfg.Client.Do(r)
+	if err != nil {
+		return res, errors.Wrap(err, "do request")
+	}
+	defer resp.Body.Close()
+
+	result, err := decodeGetPageCoverImageResponse(resp, span)
+	if err != nil {
+		return res, errors.Wrap(err, "decode response")
+	}
+
+	return result, nil
+}
+
+// GetPageImage invokes getPageImage operation.
+//
+// GET /galleries/{media_id}/{page}.{format}
+func (c *Client) GetPageImage(ctx context.Context, params GetPageImageParams) (res GetPageImageRes, err error) {
+	startTime := time.Now()
+	otelAttrs := []attribute.KeyValue{
+		otelogen.OperationID("getPageImage"),
+	}
+	ctx, span := c.cfg.Tracer.Start(ctx, "GetPageImage",
+		trace.WithAttributes(otelAttrs...),
+		trace.WithSpanKind(trace.SpanKindClient),
+	)
+	defer func() {
+		if err != nil {
+			span.RecordError(err)
+			c.errors.Add(ctx, 1, otelAttrs...)
+		} else {
+			elapsedDuration := time.Since(startTime)
+			c.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
+		}
+		span.End()
+	}()
+	c.requests.Add(ctx, 1, otelAttrs...)
+	u := uri.Clone(c.serverURL)
+	u.Path += "/galleries/"
+	{
+		// Encode "media_id" parameter.
+		e := uri.NewPathEncoder(uri.PathEncoderConfig{
+			Param:   "media_id",
+			Style:   uri.PathStyleSimple,
+			Explode: false,
+		})
+		if err := func() error {
+			return e.EncodeValue(conv.IntToString(params.MediaID))
+		}(); err != nil {
+			return res, errors.Wrap(err, "encode path")
+		}
+		u.Path += e.Result()
+	}
+	u.Path += "/"
+	{
+		// Encode "page" parameter.
+		e := uri.NewPathEncoder(uri.PathEncoderConfig{
+			Param:   "page",
+			Style:   uri.PathStyleSimple,
+			Explode: false,
+		})
+		if err := func() error {
+			return e.EncodeValue(conv.IntToString(params.Page))
+		}(); err != nil {
+			return res, errors.Wrap(err, "encode path")
+		}
+		u.Path += e.Result()
+	}
+	u.Path += "."
+	{
+		// Encode "format" parameter.
+		e := uri.NewPathEncoder(uri.PathEncoderConfig{
+			Param:   "format",
+			Style:   uri.PathStyleSimple,
+			Explode: false,
+		})
+		if err := func() error {
+			return e.EncodeValue(conv.StringToString(params.Format))
+		}(); err != nil {
+			return res, errors.Wrap(err, "encode path")
+		}
+		u.Path += e.Result()
+	}
+
+	r := ht.NewRequest(ctx, "GET", u, nil)
+	defer ht.PutRequest(r)
+
+	resp, err := c.cfg.Client.Do(r)
+	if err != nil {
+		return res, errors.Wrap(err, "do request")
+	}
+	defer resp.Body.Close()
+
+	result, err := decodeGetPageImageResponse(resp, span)
+	if err != nil {
+		return res, errors.Wrap(err, "decode response")
+	}
+
+	return result, nil
+}
+
+// GetPageThumbnailImage invokes getPageThumbnailImage operation.
+//
+// GET /galleries/{media_id}/{page}t.{format}
+func (c *Client) GetPageThumbnailImage(ctx context.Context, params GetPageThumbnailImageParams) (res GetPageThumbnailImageRes, err error) {
+	startTime := time.Now()
+	otelAttrs := []attribute.KeyValue{
+		otelogen.OperationID("getPageThumbnailImage"),
+	}
+	ctx, span := c.cfg.Tracer.Start(ctx, "GetPageThumbnailImage",
+		trace.WithAttributes(otelAttrs...),
+		trace.WithSpanKind(trace.SpanKindClient),
+	)
+	defer func() {
+		if err != nil {
+			span.RecordError(err)
+			c.errors.Add(ctx, 1, otelAttrs...)
+		} else {
+			elapsedDuration := time.Since(startTime)
+			c.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
+		}
+		span.End()
+	}()
+	c.requests.Add(ctx, 1, otelAttrs...)
+	u := uri.Clone(c.serverURL)
+	u.Path += "/galleries/"
+	{
+		// Encode "media_id" parameter.
+		e := uri.NewPathEncoder(uri.PathEncoderConfig{
+			Param:   "media_id",
+			Style:   uri.PathStyleSimple,
+			Explode: false,
+		})
+		if err := func() error {
+			return e.EncodeValue(conv.IntToString(params.MediaID))
+		}(); err != nil {
+			return res, errors.Wrap(err, "encode path")
+		}
+		u.Path += e.Result()
+	}
+	u.Path += "/"
+	{
+		// Encode "page" parameter.
+		e := uri.NewPathEncoder(uri.PathEncoderConfig{
+			Param:   "page",
+			Style:   uri.PathStyleSimple,
+			Explode: false,
+		})
+		if err := func() error {
+			return e.EncodeValue(conv.IntToString(params.Page))
+		}(); err != nil {
+			return res, errors.Wrap(err, "encode path")
+		}
+		u.Path += e.Result()
+	}
+	u.Path += "t."
+	{
+		// Encode "format" parameter.
+		e := uri.NewPathEncoder(uri.PathEncoderConfig{
+			Param:   "format",
+			Style:   uri.PathStyleSimple,
+			Explode: false,
+		})
+		if err := func() error {
+			return e.EncodeValue(conv.StringToString(params.Format))
+		}(); err != nil {
+			return res, errors.Wrap(err, "encode path")
+		}
+		u.Path += e.Result()
+	}
+
+	r := ht.NewRequest(ctx, "GET", u, nil)
+	defer ht.PutRequest(r)
+
+	resp, err := c.cfg.Client.Do(r)
+	if err != nil {
+		return res, errors.Wrap(err, "do request")
+	}
+	defer resp.Body.Close()
+
+	result, err := decodeGetPageThumbnailImageResponse(resp, span)
+	if err != nil {
+		return res, errors.Wrap(err, "decode response")
+	}
+
+	return result, nil
+}
+
 // Search invokes search operation.
 //
 // GET /api/galleries/search
diff --git a/examples/ex_manga/oas_handlers_gen.go b/examples/ex_manga/oas_handlers_gen.go
index 09246a40..c36f8bce 100644
--- a/examples/ex_manga/oas_handlers_gen.go
+++ b/examples/ex_manga/oas_handlers_gen.go
@@ -60,6 +60,141 @@ func (s *Server) handleGetBookRequest(args [1]string, w http.ResponseWriter, r *
 	s.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
 }
 
+// HandleGetPageCoverImageRequest handles getPageCoverImage operation.
+//
+// GET /galleries/{media_id}/cover.{format}
+func (s *Server) handleGetPageCoverImageRequest(args [2]string, w http.ResponseWriter, r *http.Request) {
+	startTime := time.Now()
+	otelAttrs := []attribute.KeyValue{
+		otelogen.OperationID("getPageCoverImage"),
+	}
+	ctx, span := s.cfg.Tracer.Start(r.Context(), "GetPageCoverImage",
+		trace.WithAttributes(otelAttrs...),
+		trace.WithSpanKind(trace.SpanKindServer),
+	)
+	s.requests.Add(ctx, 1, otelAttrs...)
+	defer span.End()
+
+	var err error
+	params, err := decodeGetPageCoverImageParams(args, r)
+	if err != nil {
+		err = &ogenerrors.DecodeParamsError{
+			Operation: "GetPageCoverImage",
+			Err:       err,
+		}
+		s.badRequest(ctx, w, r, span, otelAttrs, err)
+		return
+	}
+
+	response, err := s.h.GetPageCoverImage(ctx, params)
+	if err != nil {
+		span.RecordError(err)
+		span.SetStatus(codes.Error, "Internal")
+		s.errors.Add(ctx, 1, otelAttrs...)
+		s.cfg.ErrorHandler(ctx, w, r, err)
+		return
+	}
+
+	if err := encodeGetPageCoverImageResponse(response, w, span); err != nil {
+		span.RecordError(err)
+		span.SetStatus(codes.Error, "Response")
+		s.errors.Add(ctx, 1, otelAttrs...)
+		return
+	}
+	elapsedDuration := time.Since(startTime)
+	s.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
+}
+
+// HandleGetPageImageRequest handles getPageImage operation.
+//
+// GET /galleries/{media_id}/{page}.{format}
+func (s *Server) handleGetPageImageRequest(args [3]string, w http.ResponseWriter, r *http.Request) {
+	startTime := time.Now()
+	otelAttrs := []attribute.KeyValue{
+		otelogen.OperationID("getPageImage"),
+	}
+	ctx, span := s.cfg.Tracer.Start(r.Context(), "GetPageImage",
+		trace.WithAttributes(otelAttrs...),
+		trace.WithSpanKind(trace.SpanKindServer),
+	)
+	s.requests.Add(ctx, 1, otelAttrs...)
+	defer span.End()
+
+	var err error
+	params, err := decodeGetPageImageParams(args, r)
+	if err != nil {
+		err = &ogenerrors.DecodeParamsError{
+			Operation: "GetPageImage",
+			Err:       err,
+		}
+		s.badRequest(ctx, w, r, span, otelAttrs, err)
+		return
+	}
+
+	response, err := s.h.GetPageImage(ctx, params)
+	if err != nil {
+		span.RecordError(err)
+		span.SetStatus(codes.Error, "Internal")
+		s.errors.Add(ctx, 1, otelAttrs...)
+		s.cfg.ErrorHandler(ctx, w, r, err)
+		return
+	}
+
+	if err := encodeGetPageImageResponse(response, w, span); err != nil {
+		span.RecordError(err)
+		span.SetStatus(codes.Error, "Response")
+		s.errors.Add(ctx, 1, otelAttrs...)
+		return
+	}
+	elapsedDuration := time.Since(startTime)
+	s.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
+}
+
+// HandleGetPageThumbnailImageRequest handles getPageThumbnailImage operation.
+//
+// GET /galleries/{media_id}/{page}t.{format}
+func (s *Server) handleGetPageThumbnailImageRequest(args [3]string, w http.ResponseWriter, r *http.Request) {
+	startTime := time.Now()
+	otelAttrs := []attribute.KeyValue{
+		otelogen.OperationID("getPageThumbnailImage"),
+	}
+	ctx, span := s.cfg.Tracer.Start(r.Context(), "GetPageThumbnailImage",
+		trace.WithAttributes(otelAttrs...),
+		trace.WithSpanKind(trace.SpanKindServer),
+	)
+	s.requests.Add(ctx, 1, otelAttrs...)
+	defer span.End()
+
+	var err error
+	params, err := decodeGetPageThumbnailImageParams(args, r)
+	if err != nil {
+		err = &ogenerrors.DecodeParamsError{
+			Operation: "GetPageThumbnailImage",
+			Err:       err,
+		}
+		s.badRequest(ctx, w, r, span, otelAttrs, err)
+		return
+	}
+
+	response, err := s.h.GetPageThumbnailImage(ctx, params)
+	if err != nil {
+		span.RecordError(err)
+		span.SetStatus(codes.Error, "Internal")
+		s.errors.Add(ctx, 1, otelAttrs...)
+		s.cfg.ErrorHandler(ctx, w, r, err)
+		return
+	}
+
+	if err := encodeGetPageThumbnailImageResponse(response, w, span); err != nil {
+		span.RecordError(err)
+		span.SetStatus(codes.Error, "Response")
+		s.errors.Add(ctx, 1, otelAttrs...)
+		return
+	}
+	elapsedDuration := time.Since(startTime)
+	s.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...)
+}
+
 // HandleSearchRequest handles search operation.
 //
 // GET /api/galleries/search
diff --git a/examples/ex_manga/oas_interfaces_gen.go b/examples/ex_manga/oas_interfaces_gen.go
index 24c68d1b..d5d315d2 100644
--- a/examples/ex_manga/oas_interfaces_gen.go
+++ b/examples/ex_manga/oas_interfaces_gen.go
@@ -5,6 +5,18 @@ type GetBookRes interface {
 	getBookRes()
 }
 
+type GetPageCoverImageRes interface {
+	getPageCoverImageRes()
+}
+
+type GetPageImageRes interface {
+	getPageImageRes()
+}
+
+type GetPageThumbnailImageRes interface {
+	getPageThumbnailImageRes()
+}
+
 type SearchByTagIDRes interface {
 	searchByTagIDRes()
 }
diff --git a/examples/ex_manga/oas_parameters_gen.go b/examples/ex_manga/oas_parameters_gen.go
index 0e8777e8..d562e270 100644
--- a/examples/ex_manga/oas_parameters_gen.go
+++ b/examples/ex_manga/oas_parameters_gen.go
@@ -51,6 +51,306 @@ func decodeGetBookParams(args [1]string, r *http.Request) (params GetBookParams,
 	return params, nil
 }
 
+type GetPageCoverImageParams struct {
+	// ID of book.
+	MediaID int
+	// Image format.
+	Format string
+}
+
+func decodeGetPageCoverImageParams(args [2]string, r *http.Request) (params GetPageCoverImageParams, _ error) {
+	// Decode path: media_id.
+	{
+		param := args[0]
+		if len(param) > 0 {
+			d := uri.NewPathDecoder(uri.PathDecoderConfig{
+				Param:   "media_id",
+				Value:   param,
+				Style:   uri.PathStyleSimple,
+				Explode: false,
+			})
+
+			if err := func() error {
+				val, err := d.DecodeValue()
+				if err != nil {
+					return err
+				}
+
+				c, err := conv.ToInt(val)
+				if err != nil {
+					return err
+				}
+
+				params.MediaID = c
+				return nil
+			}(); err != nil {
+				return params, err
+			}
+		} else {
+			return params, errors.New("path: media_id: not specified")
+		}
+	}
+	// Set default value for path: format.
+	{
+		val := string("jpg")
+		params.Format = val
+	}
+	// Decode path: format.
+	{
+		param := args[1]
+		if len(param) > 0 {
+			d := uri.NewPathDecoder(uri.PathDecoderConfig{
+				Param:   "format",
+				Value:   param,
+				Style:   uri.PathStyleSimple,
+				Explode: false,
+			})
+
+			if err := func() error {
+				val, err := d.DecodeValue()
+				if err != nil {
+					return err
+				}
+
+				c, err := conv.ToString(val)
+				if err != nil {
+					return err
+				}
+
+				params.Format = c
+				return nil
+			}(); err != nil {
+				return params, err
+			}
+		} else {
+			return params, errors.New("path: format: not specified")
+		}
+	}
+	return params, nil
+}
+
+type GetPageImageParams struct {
+	// ID of book.
+	MediaID int
+	// Number of page.
+	Page int
+	// Image format.
+	Format string
+}
+
+func decodeGetPageImageParams(args [3]string, r *http.Request) (params GetPageImageParams, _ error) {
+	// Decode path: media_id.
+	{
+		param := args[0]
+		if len(param) > 0 {
+			d := uri.NewPathDecoder(uri.PathDecoderConfig{
+				Param:   "media_id",
+				Value:   param,
+				Style:   uri.PathStyleSimple,
+				Explode: false,
+			})
+
+			if err := func() error {
+				val, err := d.DecodeValue()
+				if err != nil {
+					return err
+				}
+
+				c, err := conv.ToInt(val)
+				if err != nil {
+					return err
+				}
+
+				params.MediaID = c
+				return nil
+			}(); err != nil {
+				return params, err
+			}
+		} else {
+			return params, errors.New("path: media_id: not specified")
+		}
+	}
+	// Decode path: page.
+	{
+		param := args[1]
+		if len(param) > 0 {
+			d := uri.NewPathDecoder(uri.PathDecoderConfig{
+				Param:   "page",
+				Value:   param,
+				Style:   uri.PathStyleSimple,
+				Explode: false,
+			})
+
+			if err := func() error {
+				val, err := d.DecodeValue()
+				if err != nil {
+					return err
+				}
+
+				c, err := conv.ToInt(val)
+				if err != nil {
+					return err
+				}
+
+				params.Page = c
+				return nil
+			}(); err != nil {
+				return params, err
+			}
+		} else {
+			return params, errors.New("path: page: not specified")
+		}
+	}
+	// Set default value for path: format.
+	{
+		val := string("jpg")
+		params.Format = val
+	}
+	// Decode path: format.
+	{
+		param := args[2]
+		if len(param) > 0 {
+			d := uri.NewPathDecoder(uri.PathDecoderConfig{
+				Param:   "format",
+				Value:   param,
+				Style:   uri.PathStyleSimple,
+				Explode: false,
+			})
+
+			if err := func() error {
+				val, err := d.DecodeValue()
+				if err != nil {
+					return err
+				}
+
+				c, err := conv.ToString(val)
+				if err != nil {
+					return err
+				}
+
+				params.Format = c
+				return nil
+			}(); err != nil {
+				return params, err
+			}
+		} else {
+			return params, errors.New("path: format: not specified")
+		}
+	}
+	return params, nil
+}
+
+type GetPageThumbnailImageParams struct {
+	// ID of book.
+	MediaID int
+	// Number of page.
+	Page int
+	// Image format.
+	Format string
+}
+
+func decodeGetPageThumbnailImageParams(args [3]string, r *http.Request) (params GetPageThumbnailImageParams, _ error) {
+	// Decode path: media_id.
+	{
+		param := args[0]
+		if len(param) > 0 {
+			d := uri.NewPathDecoder(uri.PathDecoderConfig{
+				Param:   "media_id",
+				Value:   param,
+				Style:   uri.PathStyleSimple,
+				Explode: false,
+			})
+
+			if err := func() error {
+				val, err := d.DecodeValue()
+				if err != nil {
+					return err
+				}
+
+				c, err := conv.ToInt(val)
+				if err != nil {
+					return err
+				}
+
+				params.MediaID = c
+				return nil
+			}(); err != nil {
+				return params, err
+			}
+		} else {
+			return params, errors.New("path: media_id: not specified")
+		}
+	}
+	// Decode path: page.
+	{
+		param := args[1]
+		if len(param) > 0 {
+			d := uri.NewPathDecoder(uri.PathDecoderConfig{
+				Param:   "page",
+				Value:   param,
+				Style:   uri.PathStyleSimple,
+				Explode: false,
+			})
+
+			if err := func() error {
+				val, err := d.DecodeValue()
+				if err != nil {
+					return err
+				}
+
+				c, err := conv.ToInt(val)
+				if err != nil {
+					return err
+				}
+
+				params.Page = c
+				return nil
+			}(); err != nil {
+				return params, err
+			}
+		} else {
+			return params, errors.New("path: page: not specified")
+		}
+	}
+	// Set default value for path: format.
+	{
+		val := string("jpg")
+		params.Format = val
+	}
+	// Decode path: format.
+	{
+		param := args[2]
+		if len(param) > 0 {
+			d := uri.NewPathDecoder(uri.PathDecoderConfig{
+				Param:   "format",
+				Value:   param,
+				Style:   uri.PathStyleSimple,
+				Explode: false,
+			})
+
+			if err := func() error {
+				val, err := d.DecodeValue()
+				if err != nil {
+					return err
+				}
+
+				c, err := conv.ToString(val)
+				if err != nil {
+					return err
+				}
+
+				params.Format = c
+				return nil
+			}(); err != nil {
+				return params, err
+			}
+		} else {
+			return params, errors.New("path: format: not specified")
+		}
+	}
+	return params, nil
+}
+
 type SearchParams struct {
 	// Search query.
 	// * You can search for multiple terms at the same time, and this will return only galleries that
diff --git a/examples/ex_manga/oas_response_decoders_gen.go b/examples/ex_manga/oas_response_decoders_gen.go
index 3e91144a..b1b01697 100644
--- a/examples/ex_manga/oas_response_decoders_gen.go
+++ b/examples/ex_manga/oas_response_decoders_gen.go
@@ -3,6 +3,7 @@
 package api
 
 import (
+	"bytes"
 	"io"
 	"net/http"
 
@@ -47,6 +48,72 @@ func decodeGetBookResponse(resp *http.Response, span trace.Span) (res GetBookRes
 		return res, validate.UnexpectedStatusCode(resp.StatusCode)
 	}
 }
+func decodeGetPageCoverImageResponse(resp *http.Response, span trace.Span) (res GetPageCoverImageRes, err error) {
+	switch resp.StatusCode {
+	case 200:
+		switch ct := resp.Header.Get("Content-Type"); ct {
+		case "image/*":
+			b, err := io.ReadAll(resp.Body)
+			if err != nil {
+				return res, err
+			}
+
+			return &GetPageCoverImageOK{
+				Data: bytes.NewReader(b),
+			}, nil
+		default:
+			return res, validate.InvalidContentType(ct)
+		}
+	case 403:
+		return &GetPageCoverImageForbidden{}, nil
+	default:
+		return res, validate.UnexpectedStatusCode(resp.StatusCode)
+	}
+}
+func decodeGetPageImageResponse(resp *http.Response, span trace.Span) (res GetPageImageRes, err error) {
+	switch resp.StatusCode {
+	case 200:
+		switch ct := resp.Header.Get("Content-Type"); ct {
+		case "image/*":
+			b, err := io.ReadAll(resp.Body)
+			if err != nil {
+				return res, err
+			}
+
+			return &GetPageImageOK{
+				Data: bytes.NewReader(b),
+			}, nil
+		default:
+			return res, validate.InvalidContentType(ct)
+		}
+	case 403:
+		return &GetPageImageForbidden{}, nil
+	default:
+		return res, validate.UnexpectedStatusCode(resp.StatusCode)
+	}
+}
+func decodeGetPageThumbnailImageResponse(resp *http.Response, span trace.Span) (res GetPageThumbnailImageRes, err error) {
+	switch resp.StatusCode {
+	case 200:
+		switch ct := resp.Header.Get("Content-Type"); ct {
+		case "image/*":
+			b, err := io.ReadAll(resp.Body)
+			if err != nil {
+				return res, err
+			}
+
+			return &GetPageThumbnailImageOK{
+				Data: bytes.NewReader(b),
+			}, nil
+		default:
+			return res, validate.InvalidContentType(ct)
+		}
+	case 403:
+		return &GetPageThumbnailImageForbidden{}, nil
+	default:
+		return res, validate.UnexpectedStatusCode(resp.StatusCode)
+	}
+}
 func decodeSearchResponse(resp *http.Response, span trace.Span) (res SearchRes, err error) {
 	switch resp.StatusCode {
 	case 200:
diff --git a/examples/ex_manga/oas_response_encoders_gen.go b/examples/ex_manga/oas_response_encoders_gen.go
index 9614e81f..2c82f06e 100644
--- a/examples/ex_manga/oas_response_encoders_gen.go
+++ b/examples/ex_manga/oas_response_encoders_gen.go
@@ -3,6 +3,7 @@
 package api
 
 import (
+	"io"
 	"net/http"
 
 	"github.com/go-faster/errors"
@@ -36,6 +37,69 @@ func encodeGetBookResponse(response GetBookRes, w http.ResponseWriter, span trac
 		return errors.Errorf("/api/gallery/{book_id}"+`: unexpected response type: %T`, response)
 	}
 }
+func encodeGetPageCoverImageResponse(response GetPageCoverImageRes, w http.ResponseWriter, span trace.Span) error {
+	switch response := response.(type) {
+	case *GetPageCoverImageOK:
+		w.Header().Set("Content-Type", "image/*")
+		w.WriteHeader(200)
+		span.SetStatus(codes.Ok, http.StatusText(200))
+		if _, err := io.Copy(w, response); err != nil {
+			return errors.Wrap(err, "write")
+		}
+
+		return nil
+
+	case *GetPageCoverImageForbidden:
+		w.WriteHeader(403)
+		span.SetStatus(codes.Error, http.StatusText(403))
+		return nil
+
+	default:
+		return errors.Errorf("/galleries/{media_id}/cover.{format}"+`: unexpected response type: %T`, response)
+	}
+}
+func encodeGetPageImageResponse(response GetPageImageRes, w http.ResponseWriter, span trace.Span) error {
+	switch response := response.(type) {
+	case *GetPageImageOK:
+		w.Header().Set("Content-Type", "image/*")
+		w.WriteHeader(200)
+		span.SetStatus(codes.Ok, http.StatusText(200))
+		if _, err := io.Copy(w, response); err != nil {
+			return errors.Wrap(err, "write")
+		}
+
+		return nil
+
+	case *GetPageImageForbidden:
+		w.WriteHeader(403)
+		span.SetStatus(codes.Error, http.StatusText(403))
+		return nil
+
+	default:
+		return errors.Errorf("/galleries/{media_id}/{page}.{format}"+`: unexpected response type: %T`, response)
+	}
+}
+func encodeGetPageThumbnailImageResponse(response GetPageThumbnailImageRes, w http.ResponseWriter, span trace.Span) error {
+	switch response := response.(type) {
+	case *GetPageThumbnailImageOK:
+		w.Header().Set("Content-Type", "image/*")
+		w.WriteHeader(200)
+		span.SetStatus(codes.Ok, http.StatusText(200))
+		if _, err := io.Copy(w, response); err != nil {
+			return errors.Wrap(err, "write")
+		}
+
+		return nil
+
+	case *GetPageThumbnailImageForbidden:
+		w.WriteHeader(403)
+		span.SetStatus(codes.Error, http.StatusText(403))
+		return nil
+
+	default:
+		return errors.Errorf("/galleries/{media_id}/{page}t.{format}"+`: unexpected response type: %T`, response)
+	}
+}
 func encodeSearchResponse(response SearchRes, w http.ResponseWriter, span trace.Span) error {
 	switch response := response.(type) {
 	case *SearchOKApplicationJSON:
diff --git a/examples/ex_manga/oas_router_gen.go b/examples/ex_manga/oas_router_gen.go
index 57bd32b3..2cac48bc 100644
--- a/examples/ex_manga/oas_router_gen.go
+++ b/examples/ex_manga/oas_router_gen.go
@@ -4,6 +4,7 @@ package api
 
 import (
 	"net/http"
+	"strings"
 )
 
 func (s *Server) notFound(w http.ResponseWriter, r *http.Request) {
@@ -18,7 +19,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		s.notFound(w, r)
 		return
 	}
-	args := [1]string{}
+	args := [3]string{}
 	// Static code generated router with unwrapped path search.
 	switch r.Method {
 	case "GET":
@@ -26,8 +27,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			break
 		}
 		switch elem[0] {
-		case '/': // Prefix: "/api/galler"
-			if l := len("/api/galler"); len(elem) >= l && elem[0:l] == "/api/galler" {
+		case '/': // Prefix: "/"
+			if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
 				elem = elem[l:]
 			} else {
 				break
@@ -37,8 +38,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 				break
 			}
 			switch elem[0] {
-			case 'i': // Prefix: "ies/"
-				if l := len("ies/"); len(elem) >= l && elem[0:l] == "ies/" {
+			case 'a': // Prefix: "api/galler"
+				if l := len("api/galler"); len(elem) >= l && elem[0:l] == "api/galler" {
 					elem = elem[l:]
 				} else {
 					break
@@ -48,52 +49,176 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 					break
 				}
 				switch elem[0] {
-				case 's': // Prefix: "search"
-					if l := len("search"); len(elem) >= l && elem[0:l] == "search" {
+				case 'i': // Prefix: "ies/"
+					if l := len("ies/"); len(elem) >= l && elem[0:l] == "ies/" {
 						elem = elem[l:]
 					} else {
 						break
 					}
 
 					if len(elem) == 0 {
-						// Leaf: Search
-						s.handleSearchRequest([0]string{}, w, r)
+						break
+					}
+					switch elem[0] {
+					case 's': // Prefix: "search"
+						if l := len("search"); len(elem) >= l && elem[0:l] == "search" {
+							elem = elem[l:]
+						} else {
+							break
+						}
 
-						return
+						if len(elem) == 0 {
+							// Leaf: Search
+							s.handleSearchRequest([0]string{}, w, r)
+
+							return
+						}
+					case 't': // Prefix: "tagged"
+						if l := len("tagged"); len(elem) >= l && elem[0:l] == "tagged" {
+							elem = elem[l:]
+						} else {
+							break
+						}
+
+						if len(elem) == 0 {
+							// Leaf: SearchByTagID
+							s.handleSearchByTagIDRequest([0]string{}, w, r)
+
+							return
+						}
 					}
-				case 't': // Prefix: "tagged"
-					if l := len("tagged"); len(elem) >= l && elem[0:l] == "tagged" {
+				case 'y': // Prefix: "y/"
+					if l := len("y/"); len(elem) >= l && elem[0:l] == "y/" {
 						elem = elem[l:]
 					} else {
 						break
 					}
 
+					// Param: "book_id"
+					// Leaf parameter
+					args[0] = elem
+					elem = ""
+
 					if len(elem) == 0 {
-						// Leaf: SearchByTagID
-						s.handleSearchByTagIDRequest([0]string{}, w, r)
+						// Leaf: GetBook
+						s.handleGetBookRequest([1]string{
+							args[0],
+						}, w, r)
 
 						return
 					}
 				}
-			case 'y': // Prefix: "y/"
-				if l := len("y/"); len(elem) >= l && elem[0:l] == "y/" {
+			case 'g': // Prefix: "galleries/"
+				if l := len("galleries/"); len(elem) >= l && elem[0:l] == "galleries/" {
 					elem = elem[l:]
 				} else {
 					break
 				}
 
-				// Param: "book_id"
-				// Leaf parameter
-				args[0] = elem
-				elem = ""
+				// Param: "media_id"
+				// Match until "/"
+				idx := strings.IndexByte(elem, '/')
+				if idx < 0 {
+					idx = len(elem)
+				}
+				args[0] = elem[:idx]
+				elem = elem[idx:]
 
 				if len(elem) == 0 {
-					// Leaf: GetBook
-					s.handleGetBookRequest([1]string{
-						args[0],
-					}, w, r)
+					break
+				}
+				switch elem[0] {
+				case '/': // Prefix: "/"
+					if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
+						elem = elem[l:]
+					} else {
+						break
+					}
+
+					if len(elem) == 0 {
+						break
+					}
+					switch elem[0] {
+					case 'c': // Prefix: "cover."
+						if l := len("cover."); len(elem) >= l && elem[0:l] == "cover." {
+							elem = elem[l:]
+						} else {
+							break
+						}
+
+						// Param: "format"
+						// Leaf parameter
+						args[1] = elem
+						elem = ""
+
+						if len(elem) == 0 {
+							// Leaf: GetPageCoverImage
+							s.handleGetPageCoverImageRequest([2]string{
+								args[0],
+								args[1],
+							}, w, r)
+
+							return
+						}
+					}
+					// Param: "page"
+					// Match until one of ".t"
+					idx := strings.IndexAny(elem, ".t")
+					if idx < 0 {
+						idx = len(elem)
+					}
+					args[1] = elem[:idx]
+					elem = elem[idx:]
+
+					if len(elem) == 0 {
+						break
+					}
+					switch elem[0] {
+					case '.': // Prefix: "."
+						if l := len("."); len(elem) >= l && elem[0:l] == "." {
+							elem = elem[l:]
+						} else {
+							break
+						}
+
+						// Param: "format"
+						// Leaf parameter
+						args[2] = elem
+						elem = ""
+
+						if len(elem) == 0 {
+							// Leaf: GetPageImage
+							s.handleGetPageImageRequest([3]string{
+								args[0],
+								args[1],
+								args[2],
+							}, w, r)
+
+							return
+						}
+					case 't': // Prefix: "t."
+						if l := len("t."); len(elem) >= l && elem[0:l] == "t." {
+							elem = elem[l:]
+						} else {
+							break
+						}
+
+						// Param: "format"
+						// Leaf parameter
+						args[2] = elem
+						elem = ""
+
+						if len(elem) == 0 {
+							// Leaf: GetPageThumbnailImage
+							s.handleGetPageThumbnailImageRequest([3]string{
+								args[0],
+								args[1],
+								args[2],
+							}, w, r)
 
-					return
+							return
+						}
+					}
 				}
 			}
 		}
@@ -105,7 +230,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 type Route struct {
 	name  string
 	count int
-	args  [1]string
+	args  [3]string
 }
 
 // OperationID returns OpenAPI operationId.
@@ -121,7 +246,7 @@ func (r Route) Args() []string {
 // FindRoute finds Route for given method and path.
 func (s *Server) FindRoute(method, path string) (r Route, _ bool) {
 	var (
-		args = [1]string{}
+		args = [3]string{}
 		elem = path
 	)
 	r.args = args
@@ -136,8 +261,8 @@ func (s *Server) FindRoute(method, path string) (r Route, _ bool) {
 			break
 		}
 		switch elem[0] {
-		case '/': // Prefix: "/api/galler"
-			if l := len("/api/galler"); len(elem) >= l && elem[0:l] == "/api/galler" {
+		case '/': // Prefix: "/"
+			if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
 				elem = elem[l:]
 			} else {
 				break
@@ -147,8 +272,8 @@ func (s *Server) FindRoute(method, path string) (r Route, _ bool) {
 				break
 			}
 			switch elem[0] {
-			case 'i': // Prefix: "ies/"
-				if l := len("ies/"); len(elem) >= l && elem[0:l] == "ies/" {
+			case 'a': // Prefix: "api/galler"
+				if l := len("api/galler"); len(elem) >= l && elem[0:l] == "api/galler" {
 					elem = elem[l:]
 				} else {
 					break
@@ -158,53 +283,169 @@ func (s *Server) FindRoute(method, path string) (r Route, _ bool) {
 					break
 				}
 				switch elem[0] {
-				case 's': // Prefix: "search"
-					if l := len("search"); len(elem) >= l && elem[0:l] == "search" {
+				case 'i': // Prefix: "ies/"
+					if l := len("ies/"); len(elem) >= l && elem[0:l] == "ies/" {
 						elem = elem[l:]
 					} else {
 						break
 					}
 
 					if len(elem) == 0 {
-						// Leaf: Search
-						r.name = "Search"
-						r.args = args
-						r.count = 0
-						return r, true
+						break
 					}
-				case 't': // Prefix: "tagged"
-					if l := len("tagged"); len(elem) >= l && elem[0:l] == "tagged" {
+					switch elem[0] {
+					case 's': // Prefix: "search"
+						if l := len("search"); len(elem) >= l && elem[0:l] == "search" {
+							elem = elem[l:]
+						} else {
+							break
+						}
+
+						if len(elem) == 0 {
+							// Leaf: Search
+							r.name = "Search"
+							r.args = args
+							r.count = 0
+							return r, true
+						}
+					case 't': // Prefix: "tagged"
+						if l := len("tagged"); len(elem) >= l && elem[0:l] == "tagged" {
+							elem = elem[l:]
+						} else {
+							break
+						}
+
+						if len(elem) == 0 {
+							// Leaf: SearchByTagID
+							r.name = "SearchByTagID"
+							r.args = args
+							r.count = 0
+							return r, true
+						}
+					}
+				case 'y': // Prefix: "y/"
+					if l := len("y/"); len(elem) >= l && elem[0:l] == "y/" {
 						elem = elem[l:]
 					} else {
 						break
 					}
 
+					// Param: "book_id"
+					// Leaf parameter
+					args[0] = elem
+					elem = ""
+
 					if len(elem) == 0 {
-						// Leaf: SearchByTagID
-						r.name = "SearchByTagID"
+						// Leaf: GetBook
+						r.name = "GetBook"
 						r.args = args
-						r.count = 0
+						r.count = 1
 						return r, true
 					}
 				}
-			case 'y': // Prefix: "y/"
-				if l := len("y/"); len(elem) >= l && elem[0:l] == "y/" {
+			case 'g': // Prefix: "galleries/"
+				if l := len("galleries/"); len(elem) >= l && elem[0:l] == "galleries/" {
 					elem = elem[l:]
 				} else {
 					break
 				}
 
-				// Param: "book_id"
-				// Leaf parameter
-				args[0] = elem
-				elem = ""
+				// Param: "media_id"
+				// Match until "/"
+				idx := strings.IndexByte(elem, '/')
+				if idx < 0 {
+					idx = len(elem)
+				}
+				args[0] = elem[:idx]
+				elem = elem[idx:]
 
 				if len(elem) == 0 {
-					// Leaf: GetBook
-					r.name = "GetBook"
-					r.args = args
-					r.count = 1
-					return r, true
+					break
+				}
+				switch elem[0] {
+				case '/': // Prefix: "/"
+					if l := len("/"); len(elem) >= l && elem[0:l] == "/" {
+						elem = elem[l:]
+					} else {
+						break
+					}
+
+					if len(elem) == 0 {
+						break
+					}
+					switch elem[0] {
+					case 'c': // Prefix: "cover."
+						if l := len("cover."); len(elem) >= l && elem[0:l] == "cover." {
+							elem = elem[l:]
+						} else {
+							break
+						}
+
+						// Param: "format"
+						// Leaf parameter
+						args[1] = elem
+						elem = ""
+
+						if len(elem) == 0 {
+							// Leaf: GetPageCoverImage
+							r.name = "GetPageCoverImage"
+							r.args = args
+							r.count = 2
+							return r, true
+						}
+					}
+					// Param: "page"
+					// Match until one of ".t"
+					idx := strings.IndexAny(elem, ".t")
+					if idx < 0 {
+						idx = len(elem)
+					}
+					args[1] = elem[:idx]
+					elem = elem[idx:]
+
+					if len(elem) == 0 {
+						break
+					}
+					switch elem[0] {
+					case '.': // Prefix: "."
+						if l := len("."); len(elem) >= l && elem[0:l] == "." {
+							elem = elem[l:]
+						} else {
+							break
+						}
+
+						// Param: "format"
+						// Leaf parameter
+						args[2] = elem
+						elem = ""
+
+						if len(elem) == 0 {
+							// Leaf: GetPageImage
+							r.name = "GetPageImage"
+							r.args = args
+							r.count = 3
+							return r, true
+						}
+					case 't': // Prefix: "t."
+						if l := len("t."); len(elem) >= l && elem[0:l] == "t." {
+							elem = elem[l:]
+						} else {
+							break
+						}
+
+						// Param: "format"
+						// Leaf parameter
+						args[2] = elem
+						elem = ""
+
+						if len(elem) == 0 {
+							// Leaf: GetPageThumbnailImage
+							r.name = "GetPageThumbnailImage"
+							r.args = args
+							r.count = 3
+							return r, true
+						}
+					}
 				}
 			}
 		}
diff --git a/examples/ex_manga/oas_schemas_gen.go b/examples/ex_manga/oas_schemas_gen.go
index ff737a00..797c75d5 100644
--- a/examples/ex_manga/oas_schemas_gen.go
+++ b/examples/ex_manga/oas_schemas_gen.go
@@ -2,6 +2,10 @@
 
 package api
 
+import (
+	"io"
+)
+
 // Ref: #/components/schemas/Book
 type Book struct {
 	ID           OptInt    "json:\"id\""
@@ -22,6 +26,51 @@ type GetBookForbidden struct{}
 
 func (*GetBookForbidden) getBookRes() {}
 
+// GetPageCoverImageForbidden is response for GetPageCoverImage operation.
+type GetPageCoverImageForbidden struct{}
+
+func (*GetPageCoverImageForbidden) getPageCoverImageRes() {}
+
+type GetPageCoverImageOK struct {
+	Data io.Reader
+}
+
+func (s GetPageCoverImageOK) Read(p []byte) (n int, err error) {
+	return s.Data.Read(p)
+}
+
+func (*GetPageCoverImageOK) getPageCoverImageRes() {}
+
+// GetPageImageForbidden is response for GetPageImage operation.
+type GetPageImageForbidden struct{}
+
+func (*GetPageImageForbidden) getPageImageRes() {}
+
+type GetPageImageOK struct {
+	Data io.Reader
+}
+
+func (s GetPageImageOK) Read(p []byte) (n int, err error) {
+	return s.Data.Read(p)
+}
+
+func (*GetPageImageOK) getPageImageRes() {}
+
+// GetPageThumbnailImageForbidden is response for GetPageThumbnailImage operation.
+type GetPageThumbnailImageForbidden struct{}
+
+func (*GetPageThumbnailImageForbidden) getPageThumbnailImageRes() {}
+
+type GetPageThumbnailImageOK struct {
+	Data io.Reader
+}
+
+func (s GetPageThumbnailImageOK) Read(p []byte) (n int, err error) {
+	return s.Data.Read(p)
+}
+
+func (*GetPageThumbnailImageOK) getPageThumbnailImageRes() {}
+
 // Ref: #/components/schemas/Image
 type Image struct {
 	T OptString "json:\"t\""
diff --git a/examples/ex_manga/oas_server_gen.go b/examples/ex_manga/oas_server_gen.go
index 82e57e4a..330ab9c4 100644
--- a/examples/ex_manga/oas_server_gen.go
+++ b/examples/ex_manga/oas_server_gen.go
@@ -16,6 +16,18 @@ type Handler interface {
 	//
 	// GET /api/gallery/{book_id}
 	GetBook(ctx context.Context, params GetBookParams) (GetBookRes, error)
+	// GetPageCoverImage implements getPageCoverImage operation.
+	//
+	// GET /galleries/{media_id}/cover.{format}
+	GetPageCoverImage(ctx context.Context, params GetPageCoverImageParams) (GetPageCoverImageRes, error)
+	// GetPageImage implements getPageImage operation.
+	//
+	// GET /galleries/{media_id}/{page}.{format}
+	GetPageImage(ctx context.Context, params GetPageImageParams) (GetPageImageRes, error)
+	// GetPageThumbnailImage implements getPageThumbnailImage operation.
+	//
+	// GET /galleries/{media_id}/{page}t.{format}
+	GetPageThumbnailImage(ctx context.Context, params GetPageThumbnailImageParams) (GetPageThumbnailImageRes, error)
 	// Search implements search operation.
 	//
 	// GET /api/galleries/search
diff --git a/examples/ex_manga/oas_unimplemented_gen.go b/examples/ex_manga/oas_unimplemented_gen.go
index 9ac638d4..15fdb271 100644
--- a/examples/ex_manga/oas_unimplemented_gen.go
+++ b/examples/ex_manga/oas_unimplemented_gen.go
@@ -20,6 +20,27 @@ func (UnimplementedHandler) GetBook(ctx context.Context, params GetBookParams) (
 	return r, ht.ErrNotImplemented
 }
 
+// GetPageCoverImage implements getPageCoverImage operation.
+//
+// GET /galleries/{media_id}/cover.{format}
+func (UnimplementedHandler) GetPageCoverImage(ctx context.Context, params GetPageCoverImageParams) (r GetPageCoverImageRes, _ error) {
+	return r, ht.ErrNotImplemented
+}
+
+// GetPageImage implements getPageImage operation.
+//
+// GET /galleries/{media_id}/{page}.{format}
+func (UnimplementedHandler) GetPageImage(ctx context.Context, params GetPageImageParams) (r GetPageImageRes, _ error) {
+	return r, ht.ErrNotImplemented
+}
+
+// GetPageThumbnailImage implements getPageThumbnailImage operation.
+//
+// GET /galleries/{media_id}/{page}t.{format}
+func (UnimplementedHandler) GetPageThumbnailImage(ctx context.Context, params GetPageThumbnailImageParams) (r GetPageThumbnailImageRes, _ error) {
+	return r, ht.ErrNotImplemented
+}
+
 // Search implements search operation.
 //
 // GET /api/galleries/search
diff --git a/jsonschema/parser.go b/jsonschema/parser.go
index 0b455ca9..693e2c41 100644
--- a/jsonschema/parser.go
+++ b/jsonschema/parser.go
@@ -344,7 +344,9 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx resolveCtx, hook func(*Schem
 		}), nil
 
 	case "":
-		return hook(&Schema{}), nil
+		return hook(&Schema{
+			Format: schema.Format,
+		}), nil
 
 	default:
 		return nil, errors.Errorf("unexpected schema type: %q", schema.Type)


Test patch:
diff --git a/gen_test.go b/gen_test.go
index 86103d05..3e401e95 100644
--- a/gen_test.go
+++ b/gen_test.go
@@ -102,9 +102,7 @@ func TestGenerate(t *testing.T) {
 		"sample.json": {
 			"enum format",
 		},
-		"manga.json": {
-			"unsupported content types",
-		},
+		"manga.json":            {},
 		"telegram_bot_api.json": {},
 		"gotd_bot_api.json":     {},
 		"k8s.json": {