Path parameter style support

I am trying to implement an endpoint definition with a comma separated path parameter, a List[String] (actually each String should be validated with a specific regex).
I was yet unabled to build a proper, validated path parameter definition for this. I could use a regex but I was hoping to have a better list+regex validator.

Should it be supported? Does anyone have examples?

OpenAPI Specs wrt path parameters: Parameter Serialization

Comma-separated path parameters are not supported out of the box. Manual schema and codec could be created with code like:

import sttp.tapir._
import sttp.tapir.generic.auto._

case class MyId(value: String) extends AnyVal
val idPatternValidator: Validator[String] = Validator.pattern("^[A-Z]*")
implicit val myIdSchema: Schema[MyId] = Codec.derivedValueClass[MyId].schema
implicit val myIdListCodec: Codec[String, List[MyId], TextPlain] = Codec.parsedString(_.split(",").toList.map(MyId(_)))
        .validateIterable(idPatternValidator.contramap[MyId](_.value))

val getUserEndpoint =
  endpoint.get
    .in(
      "user" / path[List[MyId]]("id")
    )
    .out(stringBody)

Such definition produces openapi schema with:

paths:
  /user/{id}:
    get:
      operationId: getUserId
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: array
          items:
            type: string
            pattern: ^[A-Z]*

Assuming that MyId is a value class (String). Let me know if you need clarifiaction on how to adjust it to different representation of the path param list element.

@kciesielski thanks for the suggestion.
In my case it is about opaque types btw.
I came up with something like:

Codec.string.map[List[Name]](_.split(",").toList.map(_.unsafeApply)(_.map(_.value).mkString(","))
  .validateIterable(Name.given_Schema_Name.validator)

as the parsedString is only ‘one-way’ from what I can see, encoding the list just does a .toString on the list
Then I also provide the schema

path[List[Name]]
  .schema(Schema.schemaForIterable[Name, List]]

If I don’t provide the schema directly it would just show as type: string. The validation pattern is not even there… Why is the validation pattern from validateIterable not being used? And why isn’t a schema for List[Name] required?

.validateIterable has a caveat described in its scaladoc:

    * Should only be used if the schema hasn't been created by `.map`ping another one, but directly from `Schema[U]`. Otherwise the shape of
    * the schema doesn't correspond to the type `T`, but to some lower-level representation of the type. This might cause invalid results at
    * run-time.

so I’m guessing this may be why it has issues in your case?

I tried a simple approach with parsedString and it seems to work as expected and produce proper specs:

opaque type Name = String
val idPatternValidator: Validator[String] = Validator.pattern("^[A-Z]*")
given myIdListCodec: Codec[String, List[Name], TextPlain] = Codec
  .parsedString(_.split(",").toList)
  .validateIterable(idPatternValidator)

val getUserEndpoint =
  endpoint.get
    .in(
      "user" / path[List[Name]]("id")
    )
    .out(stringBody)

yaml:

paths:
  /user/{id}:
    get:
      operationId: getUserId
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: array
          items:
            type: string
            pattern: ^[A-Z]*

What is the issue with parsedString1 being only ‘one-way’ in your example?

With ‘one-way’ I mean it is only for decoding. If I use the endpoint definition as a client it does a direct .toString on the List. So I need to specify how to encoding and mkString with ‘,’

I see, so without the map…, when I use

Codec.anyString(TextPlain())(raw => DecodeResult.Value(raw.split(",").toList.map(Name.unsafeApply)))(_.mkString(","))
  .validateIterable(Name.given_Schema_Name.validator)

It does produce a correct schema.

I see there is a Codec.delimited… going to explore that one.

cool, these codecs are what I need:

given Codec[String, Name, TextPlain] = Codec.parsedString(Name.unsafeApply)
  .validate(Name.given_Schema_Name.validator)
given Codec[String, Delimited[",", Name], TextPlain] = Codec.delimited
1 Like

So it is supported out-of-the-box :upside_down_face:
But I think it is missing from the documentation.

Nice! Looks like documentation indeed mentions Delimited only in context of enumerations. I’ll update the issue I created to mention this :slight_smile: