diff --git a/json/pickler/src/main/scala/sttp/tapir/json/pickler/CreateDerivedEnumerationPickler.scala b/json/pickler/src/main/scala/sttp/tapir/json/pickler/CreateDerivedEnumerationPickler.scala index f36bc01c46..4f3a459779 100644 --- a/json/pickler/src/main/scala/sttp/tapir/json/pickler/CreateDerivedEnumerationPickler.scala +++ b/json/pickler/src/main/scala/sttp/tapir/json/pickler/CreateDerivedEnumerationPickler.scala @@ -47,11 +47,11 @@ class CreateDerivedEnumerationPickler[T: ClassTag]( override lazy val writer: Writer[T] = new TaggedWriter.Node[T](childReadWriters.map(_._2.asInstanceOf[TaggedWriter[T]]): _*) { - override def findWriter(v: Any): (String, ObjectWriter[T]) = - val (t, writer) = super.findWriter(v) + override def findWriterWithKey(v: Any): (String, String, ObjectWriter[T]) = + val (tagKey, tagValue, writer) = super.findWriterWithKey(v) // Here our custom encoding transforms the value of a singleton object val overriddenTag = encode(v.asInstanceOf[T]).toString - (overriddenTag, writer) + (tagKey, overriddenTag, writer) } } new Pickler[T](tapirPickle, schema) @@ -77,10 +77,14 @@ class CreateDerivedEnumerationPickler[T: ClassTag]( // https://github.com/softwaremill/tapir/issues/3192 override lazy val writer = annotate[C]( SingletonWriter[C](null.asInstanceOf[C]), + Annotator.defaultTagKey, // not used in enumerations upickleMacros.tagName[C], Annotator.Checker.Val(upickleMacros.getSingleton[C]) ) - override lazy val reader = annotate[C](SingletonReader[C](upickleMacros.getSingleton[C]), upickleMacros.tagName[C]) + override lazy val reader = annotate[C](SingletonReader[C](upickleMacros.getSingleton[C]), + Annotator.defaultTagKey, // not used in enumerations + upickleMacros.tagName[C] + ) } (pickle.reader, pickle.writer) diff --git a/json/pickler/src/main/scala/sttp/tapir/json/pickler/Pickler.scala b/json/pickler/src/main/scala/sttp/tapir/json/pickler/Pickler.scala index ea3768d365..826e8660b9 100644 --- a/json/pickler/src/main/scala/sttp/tapir/json/pickler/Pickler.scala +++ b/json/pickler/src/main/scala/sttp/tapir/json/pickler/Pickler.scala @@ -14,10 +14,8 @@ import scala.quoted.* import scala.reflect.ClassTag import scala.util.{Failure, NotGiven, Success, Try} import java.math.{BigDecimal as JBigDecimal, BigInteger as JBigInteger} -import macros.* import scala.annotation.implicitNotFound -import sttp.tapir.json.pickler.SubtypeDiscriminator import sttp.tapir.generic.Configuration object Pickler: @@ -56,7 +54,7 @@ object Pickler: type ParamV = V val subtypeDiscriminator: SubtypeDiscriminator[T] = new CustomSubtypeDiscriminator[T] { type V = ParamV - override lazy val fieldName = c.discriminator.getOrElse(SubtypeDiscriminator.DefaultFieldName) + override lazy val fieldName = c.discriminator override def extractor = extractorFn override def asString = asStringFn override lazy val mapping = paramMapping @@ -353,8 +351,6 @@ object Pickler: val childDefaults = enrichedChildSchemas.map(_.default.map(_._1)) val tapirPickle = new TapirPickle[T] { - override def tagName = config.discriminator.getOrElse(super.tagName) - override lazy val writer: Writer[T] = macroProductW[T]( schema, @@ -367,7 +363,8 @@ object Pickler: schema, childPicklers.map([a] => (obj: a) => obj.asInstanceOf[Pickler[a]].innerUpickle.reader), childDefaults, - product + product, + config ) } Pickler[T](tapirPickle, schema) @@ -387,7 +384,6 @@ object Pickler: ): Pickler[T] = val childPicklersList = childPicklers.productIterator.toList.asInstanceOf[List[Pickler[_ <: T]]] val tapirPickle = new TapirPickle[T] { - override def tagName = subtypeDiscriminator.fieldName override lazy val writer: Writer[T] = macroSumW[T]( childPicklersList, diff --git a/json/pickler/src/main/scala/sttp/tapir/json/pickler/PicklerConfiguration.scala b/json/pickler/src/main/scala/sttp/tapir/json/pickler/PicklerConfiguration.scala index 5c5d451f65..4555db408a 100644 --- a/json/pickler/src/main/scala/sttp/tapir/json/pickler/PicklerConfiguration.scala +++ b/json/pickler/src/main/scala/sttp/tapir/json/pickler/PicklerConfiguration.scala @@ -1,9 +1,12 @@ package sttp.tapir.json.pickler import sttp.tapir.generic.Configuration +import upickle.core.Annotator final case class PicklerConfiguration(genericDerivationConfig: Configuration) { - export genericDerivationConfig.{toEncodedName, discriminator, toDiscriminatorValue} + export genericDerivationConfig.{toEncodedName, toDiscriminatorValue} + + def discriminator: String = genericDerivationConfig.discriminator.getOrElse(Annotator.defaultTagKey) def withSnakeCaseMemberNames: PicklerConfiguration = PicklerConfiguration(genericDerivationConfig.withSnakeCaseMemberNames) def withScreamingSnakeCaseMemberNames: PicklerConfiguration = PicklerConfiguration( @@ -32,6 +35,6 @@ final case class PicklerConfiguration(genericDerivationConfig: Configuration) { object PicklerConfiguration { given default: PicklerConfiguration = PicklerConfiguration( - Configuration.default.copy(discriminator = Some(SubtypeDiscriminator.DefaultFieldName)) + Configuration.default.copy(discriminator = Some(Annotator.defaultTagKey)) ) } diff --git a/json/pickler/src/main/scala/sttp/tapir/json/pickler/Readers.scala b/json/pickler/src/main/scala/sttp/tapir/json/pickler/Readers.scala index b3773a7ffe..4f383d7d0d 100644 --- a/json/pickler/src/main/scala/sttp/tapir/json/pickler/Readers.scala +++ b/json/pickler/src/main/scala/sttp/tapir/json/pickler/Readers.scala @@ -25,11 +25,17 @@ private[pickler] trait Readers extends ReadersVersionSpecific with UpickleHelper override def findReader(s: String) = if (s == leafTagValue) r else null } - override def annotate[V](rw: Reader[V], n: String) = { - LeafWrapper(new TaggedReader.Leaf[V](n, rw), rw, n) + override def annotate[V](rw: Reader[V], key: String, value: String) = { + LeafWrapper(new TaggedReader.Leaf[V](key, value, rw), rw, value) } - inline def macroProductR[T](schema: Schema[T], childReaders: Tuple, childDefaults: List[Option[Any]], m: Mirror.ProductOf[T]): Reader[T] = + inline def macroProductR[T]( + schema: Schema[T], + childReaders: Tuple, + childDefaults: List[Option[Any]], + m: Mirror.ProductOf[T], + config: PicklerConfiguration + ): Reader[T] = val schemaFields = schema.schemaType.asInstanceOf[SchemaType.SProduct[T]].fields val reader = new CaseClassReadereader[T](upickleMacros.paramsCount[T], upickleMacros.checkErrorMissingKeysCount[T]()) { @@ -44,8 +50,9 @@ private[pickler] trait Readers extends ReadersVersionSpecific with UpickleHelper } } - inline if upickleMacros.isSingleton[T] then annotate[T](SingletonReader[T](upickleMacros.getSingleton[T]), upickleMacros.tagName[T]) - else if upickleMacros.isMemberOfSealedHierarchy[T] then annotate[T](reader, upickleMacros.tagName[T]) + inline if upickleMacros.isSingleton[T] then + annotate[T](SingletonReader[T](upickleMacros.getSingleton[T]), config.discriminator, upickleMacros.tagName[T]) + else if upickleMacros.isMemberOfSealedHierarchy[T] then annotate[T](reader, config.discriminator, upickleMacros.tagName[T]) else reader inline def macroSumR[T](childPicklers: List[Pickler[_]], subtypeDiscriminator: SubtypeDiscriminator[T]): Reader[T] = @@ -60,18 +67,20 @@ private[pickler] trait Readers extends ReadersVersionSpecific with UpickleHelper .map { case (k, v) => (k, v.innerUpickle.reader) } .map { case (k, leaf) if leaf.isInstanceOf[LeafWrapper[_]] => - TaggedReader.Leaf[T](discriminator.asString(k), leaf.asInstanceOf[LeafWrapper[_]].r.asInstanceOf[Reader[T]]) + TaggedReader + .Leaf[T](discriminator.fieldName, discriminator.asString(k), leaf.asInstanceOf[LeafWrapper[_]].r.asInstanceOf[Reader[T]]) case (_, otherKindOfReader) => otherKindOfReader } - new TaggedReader.Node[T](readersFromMapping.asInstanceOf[Seq[TaggedReader[T]]]: _*) + new TaggedReader.Node[T](discriminator.fieldName, readersFromMapping.asInstanceOf[Seq[TaggedReader[T]]]: _*) case discriminator: DefaultSubtypeDiscriminator[T] => val readers = childPicklers.map(cp => { (cp.schema.name, cp.innerUpickle.reader) match { case (Some(sName), wrappedReader: Readers#LeafWrapper[_]) => TaggedReader.Leaf[T]( + discriminator.fieldName, discriminator.toValue(sName), wrappedReader.r.asInstanceOf[Reader[T]] ) @@ -79,6 +88,6 @@ private[pickler] trait Readers extends ReadersVersionSpecific with UpickleHelper cp.innerUpickle.reader.asInstanceOf[Reader[T]] } }) - Reader.merge(readers: _*) + Reader.merge(subtypeDiscriminator.fieldName, readers: _*) } } diff --git a/json/pickler/src/main/scala/sttp/tapir/json/pickler/SubtypeDiscriminator.scala b/json/pickler/src/main/scala/sttp/tapir/json/pickler/SubtypeDiscriminator.scala index fa866695b7..ef3e6fdf28 100644 --- a/json/pickler/src/main/scala/sttp/tapir/json/pickler/SubtypeDiscriminator.scala +++ b/json/pickler/src/main/scala/sttp/tapir/json/pickler/SubtypeDiscriminator.scala @@ -6,9 +6,6 @@ import sttp.tapir.Schema.SName private[pickler] sealed trait SubtypeDiscriminator[T]: def fieldName: String -object SubtypeDiscriminator: - val DefaultFieldName = "$type" - /** Describes non-standard encoding/decoding for subtypes in sealed hierarchies. Allows specifying an extractor function, for example to * read subtype discriminator from a field. Requires also mapping in the opposite direction, to specify how to read particular * discriminator values into concrete subtype picklers. @@ -27,4 +24,4 @@ private[pickler] case class DefaultSubtypeDiscriminator[T](fieldName: String, to private[pickler] object DefaultSubtypeDiscriminator: def apply[T](config: PicklerConfiguration): DefaultSubtypeDiscriminator[T] = - new DefaultSubtypeDiscriminator[T](config.discriminator.getOrElse(SubtypeDiscriminator.DefaultFieldName), config.toDiscriminatorValue) + new DefaultSubtypeDiscriminator[T](config.discriminator, config.toDiscriminatorValue) diff --git a/json/pickler/src/main/scala/sttp/tapir/json/pickler/Writers.scala b/json/pickler/src/main/scala/sttp/tapir/json/pickler/Writers.scala index 8fbd7a104c..a18df4157e 100644 --- a/json/pickler/src/main/scala/sttp/tapir/json/pickler/Writers.scala +++ b/json/pickler/src/main/scala/sttp/tapir/json/pickler/Writers.scala @@ -68,13 +68,14 @@ private[pickler] trait Writers extends WritersVersionSpecific with UpickleHelper inline if upickleMacros.isMemberOfSealedHierarchy[T] && !isEnumeration[T] then annotate[T]( writer, + config.discriminator, schema.name.map(config.toDiscriminatorValue).getOrElse(upickleMacros.tagName[T]), Annotator.Checker.Cls(implicitly[ClassTag[T]].runtimeClass) ) // tagName is responsible for extracting the @tag annotation meaning the discriminator value else if upickleMacros.isSingleton[T] then // moved after "if MemberOfSealed" to handle case objects in hierarchy as case classes - with discriminator, for consistency // here we handle enums - annotate[T](SingletonWriter[T](null.asInstanceOf[T]), upickleMacros.tagName[T], Annotator.Checker.Val(upickleMacros.getSingleton[T])) + annotate[T](SingletonWriter[T](null.asInstanceOf[T]), config.discriminator, upickleMacros.tagName[T], Annotator.Checker.Val(upickleMacros.getSingleton[T])) else writer inline def macroSumW[T: ClassTag](childPicklers: => List[Pickler[? <: T]], subtypeDiscriminator: SubtypeDiscriminator[T])(using @@ -84,14 +85,14 @@ private[pickler] trait Writers extends WritersVersionSpecific with UpickleHelper val writers: List[TaggedWriter[_ <: T]] = childPicklers.map(_.innerUpickle.writer.asInstanceOf[TaggedWriter[_ <: T]]) new TaggedWriter.Node[T](writers: _*) { - override def findWriter(v: Any): (String, ObjectWriter[T]) = { + override def findWriterWithKey(v: Any): (String, String, ObjectWriter[T]) = { subtypeDiscriminator match { case discriminator: CustomSubtypeDiscriminator[T] => - val (tag, w) = super.findWriter(v) + val (tagKey, tagValue, w) = super.findWriterWithKey(v) val overriddenTag = discriminator.writeUnsafe(v) // here we use our discirminator instead of uPickle's - (overriddenTag, w) + (tagKey, overriddenTag, w) case _ => - super.findWriter(v) + super.findWriterWithKey(v) } } } diff --git a/project/Versions.scala b/project/Versions.scala index f3d8571938..d3a2f54bc9 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -17,7 +17,7 @@ object Versions { val pekkoHttp = "1.0.1" val pekkoStreams = "1.0.2" val swaggerUi = "5.17.2" - val upickle = "3.3.0" + val upickle = "3.3.1" val playJson = "3.0.1" val play29Json = "3.0.3" val finatra = "24.2.0"