Closed sbrunk closed 2 years ago
I have been experimenting with the following setup. Not quite sure about the format and description handling yet.
implicit def schemaForMap[K: KeyEncoder: KeyDecoder: Schema, V: Schema]: Schema[Map[K, V]] = {
val keySchema = implicitly[Schema[K]]
val mapSchema = Schema
.schemaForMap[V]
.contramap[Map[K, V]](_.map { case (k, v) => (KeyEncoder[K].apply(k), v) })
val mapSchemaWithFormat = keySchema.format
.orElse(mapSchema.format)
.fold(mapSchema)(mapSchema.format(_))
keySchema.description
.orElse(mapSchema.description)
.fold(mapSchemaWithFormat)(mapSchemaWithFormat.description(_))
}
I have been experimenting with the following setup. Not quite sure about the format and description handling yet.
implicit def schemaForMap[K: KeyEncoder: KeyDecoder: Schema, V: Schema]: Schema[Map[K, V]] = { val keySchema = implicitly[Schema[K]] val mapSchema = Schema .schemaForMap[V] .contramap[Map[K, V]](_.map { case (k, v) => (KeyEncoder[K].apply(k), v) }) val mapSchemaWithFormat = keySchema.format .orElse(mapSchema.format) .fold(mapSchema)(mapSchema.format(_)) keySchema.description .orElse(mapSchema.description) .fold(mapSchemaWithFormat)(mapSchemaWithFormat.description(_)) }
If this workaround used in multiple places with differently typed Maps - in Swagger it will show only one variant (Map_V) for every appearances. Maybe it is because of #763 .
Yeah, it seems so. A workaround is to add a TypeTag constraint to get the full type name:
def fixSchemaName[Original, Additional: TypeTag](schema: Schema[Original]): Schema[Original] = {
import sttp.tapir.SchemaType.{SOpenProduct, SProduct}
val typesNames: String = {
val typeArgs = typeOf[Additional].typeArgs
.map(_.typeSymbol.name.toString)
s"${typeOf[Additional].typeSymbol.name.toString}${if (typeArgs.nonEmpty) typeArgs.mkString("[", ",", "]")
else ""}"
}
schema.copy(schemaType = schema.schemaType match {
case a: SOpenProduct =>
a.copy(info = a.info.copy(typeParameterShortNames = List(typesNames)))
case a: SProduct =>
a.copy(info = a.info.copy(typeParameterShortNames = List(typesNames)))
case a => a
})
}
/**
* If we know [[K]] is a KeyEncoder, KeyDecoder then the representation is a String that
* is usable in a Json Map.
*
* @tparam K Key
* @tparam V Value
* @return Schema for Map[K, V]
*/
@SuppressWarnings(
Array("org.wartremover.warts.AsInstanceOf", "org.wartremover.warts.OptionPartial")
)
implicit def schemaForMap[K: KeyEncoder: KeyDecoder: Schema, V: Schema: TypeTag]
: Schema[Map[K, V]] = {
val keySchema = implicitly[Schema[K]]
val stringKeySchema = Schema.schemaForMap[V]
val schema_ = stringKeySchema.asInstanceOf[Schema[Map[K, V]]]
val schema = fixSchemaName[Map[K, V], V](schema_)
val schemaWithFormat = keySchema.format
.fold(schema)(schema.format(_))
keySchema.description
.fold(schemaWithFormat)(schemaWithFormat.format(_))
}
So to use this I should create my own package in my project to redefine the new implicit ? Or should I somehow patch the tapir dependency that I import ?
Any implicits that you import or have defined next to where you use them, have precedence over those in the companion objects, so you shouldn't have to patch tapir :)
might also apply to newtypes/opaque types
I can confirm this for opaque types
See: https://github.com/softwaremill/tapir/pull/1404
The proposed solution does not define an implicit, but a method to create a map schema given a function to convert the keys to strings.
We could probably do a better job with value classes and opaque types, though.
Keeping this open to implement automatic derivation when keys are value classes / opaque types
On second (or third :) ) thought, I don't think we can do much more for value classes etc., as we need the key-to-string conversion function anyway. Hence closing, for related feature requests please open new issues.
@adamw please what is the outcome for this because I am facing the same issue I have something is this
final case class Info(value: String) extends AnyVal
case class User(name: String, data: Map[Info, Set[String]])
but the map key in schemaForMap is fixed to String:
def schemaForMap[V: Schema]: Schema[Map[String, V]] = macro SchemaMapMacro.schemaForMap[Map[String, V], V]
@SarpongAbasimi you should use this variant: def schemaForMap[K, V: Schema](keyToString: K => String): Schema[Map[K, V]]
@adamw thank you. so this is it.
implicit val rolesSchema: Schema[Map[Info, Set[String]]] = Schema.schemaForMap[Info, Set[String]](_.value)
implicit val userSchema: Schema[User] = Schema.derived[User]
As discussed on Gitter (a while ago), extending
SchemaMapMacro.schemaForMap
to support non-stringMap
keys would help for cases where string keys are wrapped in values classes (might also apply to newtypes/opaque types).Consider the following example:
Currently, this doesn't work because the map key in
schemaForMap
is fixed toString
: https://github.com/softwaremill/tapir/blob/12c641fe73e5658fb5905477dd6271123662ee1c/core/src/main/scala/sttp/tapir/Schema.scala#L142