tapir
tapir copied to clipboard
[Feature] deriving encodedNames from an ordered list
I think it would be nice if we can do something like this:
case class SomeThing(data: String, typeStr: String, uid: String, )
implicit lazy val sSomeThing: Schema[SomeThing] = Schema.derivedWithEncodedTypes3("data", "type", "id")
Similar is being done by Circe here:
import io.circe.{ Decoder, Encoder }
case class User(id: Long, firstName: String, lastName: String)
implicit val decodeUser: Decoder[User] = Decoder.forProduct3("id", "first_name", "last_name")(User.apply)
implicit val encodeUser: Encoder[User] = Encoder.forProduct3("id", "first_name", "last_name")(u => (u.id, u.firstName, u.lastName))
Alternatively, using a list, instead of having to specify the number of arguments, would be even better, like this:
val SomeThingList = List("data", "type", "id")
implicit lazy val sSomeThing: Schema[SomeThing] = Schema.derivedWithEncodedTypes3("data", "type", "id")
Perhaps something like this, which seems to work for me:
def makeSchema[T](schema: Schema[T], list: List[String]) = {
val s = schema.schemaType match {
case s @ SProduct(_) =>
s.copy(fields = s.fields.mapWithIndex { case (field, i) => SProductField[T, field.FieldType](field.name.copy(encodedName = list.get(i).get), field.schema, field.get) })
case x => x
}
schema.copy(schemaType = s)
}
and used like this:
makeSchema(Schema.derived[SomeThing], List("data", "id", "type")
Hi @decoursin, thank your for submitting the idea. I think Circe uses specific number of arguments, because they don't want to allow unsafe executions. The list.get(i).get
call can throw an exception, and we also would like to avoid it in tapir. Could you describe your use case, where you need such custom name derivation?
BTW how about using @encodedName
? https://tapir.softwaremill.com/en/latest/endpoint/schemas.html?highlight=encodedName#customising-derived-schemas
Hi @kciesielski thanks for your comments. The problem essentially stems from the fact that derivations (or json conversions, however you want to call them) are being done twice.
When I'm writing a backend API, of course, I absolutely want my API documentation to be perfectly inline with the real json encoder/decoders. Any discrepancy can cause huge problems: imagine a 3rd party App Developer who is trying to build an App against your backend by only using the API documentation, and their's misalignment with the documentation and the real json encoding/decoding - that could cause serious problems.
#2923 would obviously be the ideal solution, but unfortunately both it's not completed yet and it would only pertain to one json library.
Imagine if I could do this:
implicit val sUser: Schema[User] = Schema.deriveByType3("id", "first_name", "last_name")
implicit val decodeUser: Decoder[User] = Decoder.forProduct3("id", "first_name", "last_name")(User.apply)
implicit val encodeUser: Encoder[User] = Encoder.forProduct3("id", "first_name", "last_name")(u => (u.id, u.firstName, u.lastName))
When it's coded like that ^, I can immediately see with my eyes that the json-specific-library encoding/decodings are exactly the same as the Schema ones. This would be ideal, and only #2923 would be a better solution in my opinion.
The annotation @encodedName
is also a valid solution, but it's inferior in my opinion because when you have hundreds of case classes with some having 20 parameters, it's far more tedious and uncertain to know if the Schema and json encoders/decoders are exactly the same, and this creates a much greater chance of mistake. Here below is for a visual comparison:
implicit val decodeUser: Decoder[User] = Decoder.forProduct3("id", "first_name", "last_name")(User.apply)
implicit val encodeUser: Encoder[User] = Encoder.forProduct3("id", "first_name", "last_name")(u => (u.id, u.firstName, u.lastName))
case class User(id: String, @encodedName("first_name") firstName: Option[String] = None, @encodedName("last_name") lastName: Option[String] = None)
and @encodedName
intermixes business code with documentation - it's nice to have these separate, and ruins the nice, clean bare case class User(...)
.
@decoursin I think that there's no reason why we couldn't support this as an additional option for customising schemas. I'm just thinking about naming - we would probably need a general private macro accepting a list of field names, plus arity-specific methods. We've got derived
as the default name, so probably something along these lines. A verbose option, like derivedUsingFieldNames3
or derivedWithFieldNames3
or derivedProduct3
, wdyt?
There's also an even more general option, to provide the metadata for each field (such as deprecation, default values, validators etc.). This could have the signature e.g. derivedUsing3(sa1: SchemaAnnotations[_], sa2: SchemaAnnotations[_], sa3: SchemaAnnotations[_])
or derivedProduct3(...)
. That way we could give a way to provide all of the available metadata externally. We would have to provide a nice way to construct the SchemaAnnotations
instances.
Of course, this could still be achieved using schema customisation, as desribed here, so using .modify
, so maybe it's not worth the trouble. Probably metadata other than custom names are comparatively rare so writing sth like Schema.derivedProduct3(...).modify(_.firstName)(_.description(...))
would be sufficient maybe?
Of course, this could still be achieved using schema customisation, as desribed here, so using .modify, so maybe it's not worth the trouble. Probably metadata other than custom names are comparatively rare so writing sth like Schema.derivedProduct3(...).modify(.firstName)(.description(...)) would be sufficient maybe?
I would agree with that statement. I think the @encodedNames
is the most important, and as long as there's any another way for users to add their additional schema modifications, like by using modify
afterwards that would be plenty good.
I'm just thinking about naming - we would probably need a general private macro accepting a list of field names, plus arity-specific methods. We've got derived as the default name, so probably something along these lines. A verbose option, like derivedUsingFieldNames3 or derivedWithFieldNames3 or derivedProduct3, wdyt?
It's difficult for me to say, I would be happy with any naming choice you make. I would perhaps stay away from Product3
because it's a little be too vague for this scenario, but it's up to you. Perhaps even derivedWithName3
or else derivedWithFieldName3
.
Thanks!
Also, if I can help at all with this feature, please do let me know. The coding might be a little bit outside of my abilities, but I'm happy to try. I can definitely do the documentation if you'd like
derivedWithFieldNames
it is ;)
If you would like to try working on this, please do :) Macros aren't that scary, esp in Scala 3.
I definitely don't have time to do Macros in Scala 3 lol, sorry. I haven't touched Scala 3 yet, and know nothing about Macros. That would take me too long given other things I need to get done. If it sits around though for a year or two I might know enough to do it then :)