softwaremill / softwaremill/tapir

[BUG] incorrect names for newtyped map schemas

Open
#3,835 2 comments 0 reactions 1 assignee View on GitHub

@kciesielski is already working on this.

Since Jun 12, 2024.

Dominant language
Scala
Stars
1.5k
Forks
468
Avg merge
5h 37m
Merged PRs (30d)
34

Description

Tapir version: 1.10.8

Scala version: 2.13.14

Describe the bug
both Keys and Values are incorrectly named in map schemas - most probably also in other cases

resulting docs for code example:

openapi: 3.1.0
info:
  title: test
  version: v1
paths:
  /test/1:
    put:
      operationId: test1
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Map_Type_Type'
        required: true
      responses:
        '200':
          description: ''
        '400':
          description: 'Invalid value for: body'
          content:
            text/plain:
              schema:
                type: string
  /test/2:
    put:
      operationId: test2
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Map_Type_Type1'
        required: true
      responses:
        '200':
          description: ''
        '400':
          description: 'Invalid value for: body'
          content:
            text/plain:
              schema:
                type: string
  /test/3:
    put:
      operationId: test3
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Map_Type_Type2'
        required: true
      responses:
        '200':
          description: ''
        '400':
          description: 'Invalid value for: body'
          content:
            text/plain:
              schema:
                type: string
components:
  schemas:
    Map_Type_Type:
      title: Map_Type_Type
      type: object
      additionalProperties:
        type: integer
        format: int32
    Map_Type_Type1:
      title: Map_Type_Type
      type: object
      additionalProperties:
        type: integer
        format: int32
    Map_Type_Type2:
      title: Map_Type_Type
      type: object
      additionalProperties:
        type: integer
        format: int32

How to reproduce?


package com.test

import com.test.Defs._
import eu.timepit.refined.types.numeric.PosInt
import eu.timepit.refined.types.string.NonEmptyString
import io.circe.refined._
import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}
import io.estatico.newtype.Coercible
import io.estatico.newtype.macros.newtype
import sttp.apispec.openapi.circe.yaml.RichOpenAPI
import sttp.tapir._
import sttp.tapir.codec.newtype.TapirCodecNewType
import sttp.tapir.codec.refined.TapirCodecRefined
import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter
import sttp.tapir.json.circe.jsonBody

object Test extends App  with TapirCodecRefined with TapirCodecNewType  with CirceCodecRefined with CirceNewtype {

  lazy implicit val map1Schema: Schema[Map[UserId, VeryImportantCount]] = Schema.schemaForMap[UserId, VeryImportantCount](_.value.value)
  lazy implicit val map2Schema: Schema[Map[AdminId, VeryImportantCount]] = Schema.schemaForMap[AdminId, VeryImportantCount](_.value.value)
  lazy implicit val map3Schema: Schema[Map[IntId, VeryImportantCount]] = Schema.schemaForMap[IntId, VeryImportantCount](_.value.value.toString)

  val endpoint1 = endpoint
    .put
    .name("test1")
    .in("test" / "1")
    .in(jsonBody[Map[UserId, VeryImportantCount]])

  val endpoint2 = endpoint
    .put
    .name("test2")
    .in("test" / "2")
    .in(jsonBody[Map[AdminId, VeryImportantCount]])

  val endpoint3 = endpoint
    .put
    .name("test3")
    .in("test" / "3")
    .in(jsonBody[Map[IntId, VeryImportantCount]])

  val endpoints = List(endpoint1, endpoint2, endpoint3)

  val yamlString: String = OpenAPIDocsInterpreter()
    .toOpenAPI(endpoints, "test", "v1")
    .toYaml

  println(yamlString)

}

object Defs {
  @newtype final case class UserId(value: NonEmptyString)
  @newtype final case class AdminId(value: NonEmptyString)
  @newtype final case class IntId(value: PosInt)

  @newtype final case class VeryImportantCount(value: Int)
}

trait CirceNewtype {
  implicit def coerceDecoder[R, N](
    implicit
    ev: Coercible[Decoder[R], Decoder[N]],
    R: Decoder[R],
  ): Decoder[N] = ev(R)

  implicit def coerceEncoder[R, N](
    implicit
    ev: Coercible[Encoder[R], Encoder[N]],
    R: Encoder[R],
  ): Encoder[N] = ev(R)

  implicit def coerceKeyDecoder[R, N](
    implicit
    ev: Coercible[KeyDecoder[R], KeyDecoder[N]],
    R: KeyDecoder[R],
  ): KeyDecoder[N] = ev(R)

  implicit def coerceKeyEncoder[R, N](
    implicit
    ev: Coercible[KeyEncoder[R], KeyEncoder[N]],
    R: KeyEncoder[R],
  ): KeyEncoder[N] = ev(R)

  implicit def coerceAsObjectEncoder[A, B](
    implicit
    ev: Coercible[Encoder.AsObject[A], Encoder.AsObject[B]],
    encA: Encoder.AsObject[A],
  ): Encoder.AsObject[B] = ev(Encoder.AsObject[A])
}

Additional information

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.