Custom Recursive Schemas

Let’s assume I need to make some kind of custom coproduct schema that is also recursive and it is neither wrapped nor it has discriminator - let’s say for API compatibility

example:

model

  type Tree[A] = A Or Node[A]

  final case class Node[A](vals: List[Tree[A]])

  sealed trait Or[+A, +B]
  final case class OrLeft[+A, +B](left: A) extends Or[A, B]
  final case class OrRight[+A, +B](right: B) extends Or[A, B]

  final case class WrappedString(value: String)
  final case class WrappedInt(value: Int)

what I want to achieve is a schema that is oneOf: [A, Node[A]]


because it is recursive I ASSUME I need to handle it with refs @line - 42

tho because I am handling it with refs it seems that the schema for Node is not generated / picked up by tapir and we get not resolved ref for that schema.

this is solvable by:

  val dummyEndpoints = endpoint
    .get
    .name("dummy endpoint to make docs work")
    .in("dummy")
    .in(jsonBody[Node[WrappedString]])
    .in(jsonBody[Node[WrappedInt]])

but welp I do not really want to be doing that :smile:

so:

  1. is there a better way to handle custom recursive coproduct schemas?
  2. is that need for dummyEndpoint expected / solvable in a better way

@lgmyrek thanks for the Scastie, it’s really helpful :slight_smile: The model indeed looks tricky, could you show a few examples of valid JSONs? I assume you need to adjust to some existing API?

let’s assume that WrappedInt and WrappedString have deserializers and schemas that produce primitive types instead of this

{
  "value": 1
}

for the sake os slightly more readable example


so endpoint1 COULD consume JUST value

3

OR tree

{
  "vals": [
    3,
    4,
    {
      "vals": [
        {
          "vals": [5]
        },
        6,
        7
      ]
    },
    8
  ]
}


if you replace all primitive ints in that example to:

{
  "value": $int
}

then that example will match scastie

1 Like

we could implement such encoder / decoder in eg circe somewhat like:

implicit def decoderForAOrNodeA[A: Decoder]: Decoder[A Or Node[A]] = 
  Decoder[A].map(OrLeft).or(Decoder[Node[A]].map(OrRight))

implicit def encoderForAOrNodeA[A: Encoder]: Encoder[A Or Node[A]] = {
  case OrLeft(a) => a.asJson
  case OrRight(aNode) => aNode.asJson
}

@kciesielski I’ve found yet another issue that happens with extended version of this example.

    Nodes_WrappedString:
      title: Nodes_WrappedString
      oneOf:
      - $ref: '#/components/schemas/Node_WrappedString'
      discriminator:
        propertyName: type
        mapping:
          Node: Defs.Node

has proper reference in oneOf, but moments later breaks on mapping.

dummy hack no longer fixes it.

Want me to create an issue on tapir for those?

Yes, it would be great if you could create one for the mapping.