diff --git a/upickle/core/src/upickle/core/Types.scala b/upickle/core/src/upickle/core/Types.scala index 0da1b7ab9..74beaa53f 100644 --- a/upickle/core/src/upickle/core/Types.scala +++ b/upickle/core/src/upickle/core/Types.scala @@ -31,10 +31,12 @@ trait Types{ types => abstract class Delegate[T](other: Visitor[Any, T]) extends Visitor.Delegate[Any, T](other) with ReadWriter[T] - def merge[T](rws: ReadWriter[_ <: T]*): TaggedReadWriter[T] = { - new TaggedReadWriter.Node(rws.asInstanceOf[Seq[TaggedReadWriter[T]]]:_*) + def merge[T](tagKey: String, rws: ReadWriter[_ <: T]*): TaggedReadWriter[T] = { + new TaggedReadWriter.Node(tagKey, rws.asInstanceOf[Seq[TaggedReadWriter[T]]]:_*) } + def merge[T](rws: ReadWriter[_ <: T]*): TaggedReadWriter[T] = merge(Annotator.defaultTagKey, rws:_*) + implicit def join[T](implicit r0: Reader[T], w0: Writer[T]): ReadWriter[T] = (r0, w0) match{ // Make sure we preserve the tagged-ness of the Readers/Writers being // pulled in; we need to do this because the macros that generate tagged @@ -45,9 +47,12 @@ trait Types{ types => case (r1: TaggedReader[T], w1: TaggedWriter[T]) => new TaggedReadWriter[T] { + private[upickle] override def tagKey = r1.tagKey override def isJsonDictKey = w0.isJsonDictKey def findReader(s: String) = r1.findReader(s) + @deprecated("Not used, left for binary compatibility") def findWriter(v: Any) = w1.findWriter(v) + override def findWriterWithKey(v: Any) = w1.findWriterWithKey(v) } case _ => @@ -104,9 +109,11 @@ trait Types{ types => override def visitArray(length: Int, index: Int) = super.visitArray(length, index).asInstanceOf[ArrVisitor[Any, Z]] } - def merge[T](readers0: Reader[_ <: T]*) = { - new TaggedReader.Node(readers0.asInstanceOf[Seq[TaggedReader[T]]]:_*) + def merge[T](tagKey: String, readers0: Reader[_ <: T]*): TaggedReader.Node[T] = { + new TaggedReader.Node(tagKey, readers0.asInstanceOf[Seq[TaggedReader[T]]]:_*) } + + def merge[T](readers0: Reader[_ <: T]*): TaggedReader.Node[T] = merge(Annotator.defaultTagKey, readers0:_*) } /** @@ -147,6 +154,8 @@ trait Types{ types => } trait TaggedReader[T] extends SimpleReader[T]{ + private[upickle] def tagKey: String = Annotator.defaultTagKey + def findReader(s: String): Reader[T] override def expectedMsg = taggedExpectedMsg @@ -160,34 +169,65 @@ trait Types{ types => } } object TaggedReader{ - class Leaf[T](tag: String, r: Reader[T]) extends TaggedReader[T]{ - def findReader(s: String) = if (s == tag) r else null + class Leaf[T](private[upickle] override val tagKey: String, tagValue: String, r: Reader[T]) extends TaggedReader[T]{ + @deprecated("Not used, left for binary compatibility") + def this(tag: String, r: Reader[T]) = this(Annotator.defaultTagKey, tag, r) + + def findReader(s: String) = if (s == tagValue) r else null } - class Node[T](rs: TaggedReader[_ <: T]*) extends TaggedReader[T]{ + class Node[T](private[upickle] override val tagKey: String, rs: TaggedReader[_ <: T]*) extends TaggedReader[T]{ + @deprecated("Not used, left for binary compatibility") + def this(rs: TaggedReader[_ <: T]*) = this(Annotator.defaultTagKey, rs:_*) + def findReader(s: String) = scanChildren(rs)(_.findReader(s)).asInstanceOf[Reader[T]] } } trait TaggedWriter[T] extends Writer[T]{ + @deprecated("Not used, left for binary compatibility") def findWriter(v: Any): (String, ObjectWriter[T]) - def write0[R](out: Visitor[_, R], v: T): R = { + + // Calling deprecated method to maintain binary compatibility + @annotation.nowarn("msg=deprecated") + def findWriterWithKey(v: Any): (String, String, ObjectWriter[T]) = { val (tag, w) = findWriter(v) - taggedWrite(w, tag, out, v) + (Annotator.defaultTagKey, tag, w) + } + + def write0[R](out: Visitor[_, R], v: T): R = { + val (tagKey, tagValue, w) = findWriterWithKey(v) + taggedWrite(w, tagKey, tagValue, out, v) } } object TaggedWriter{ - class Leaf[T](checker: Annotator.Checker, tag: String, r: ObjectWriter[T]) extends TaggedWriter[T]{ + class Leaf[T](checker: Annotator.Checker, tagKey: String, tagValue: String, r: ObjectWriter[T]) extends TaggedWriter[T]{ + @deprecated("Not used, left for binary compatibility") + def this(checker: Annotator.Checker, tag: String, r: ObjectWriter[T]) = + this(checker, Annotator.defaultTagKey, tag, r) + + @deprecated("Not used, left for binary compatibility") def findWriter(v: Any) = { checker match{ - case Annotator.Checker.Cls(c) if c.isInstance(v) => tag -> r - case Annotator.Checker.Val(v0) if v0 == v => tag -> r + case Annotator.Checker.Cls(c) if c.isInstance(v) => tagValue -> r + case Annotator.Checker.Val(v0) if v0 == v => tagValue -> r + case _ => null + } + } + + override def findWriterWithKey(v: Any) = { + checker match{ + case Annotator.Checker.Cls(c) if c.isInstance(v) => (tagKey, tagValue, r) + case Annotator.Checker.Val(v0) if v0 == v => (tagKey, tagValue, r) case _ => null } } } class Node[T](rs: TaggedWriter[_ <: T]*) extends TaggedWriter[T]{ + @deprecated("Not used, left for binary compatibility") def findWriter(v: Any) = scanChildren(rs)(_.findWriter(v)).asInstanceOf[(String, ObjectWriter[T])] + override def findWriterWithKey(v: Any) = + scanChildren(rs)(_.findWriterWithKey(v)).asInstanceOf[(String, String, ObjectWriter[T])] } } @@ -197,16 +237,30 @@ trait Types{ types => } object TaggedReadWriter{ - class Leaf[T](c: ClassTag[_], tag: String, r: ObjectWriter[T] with Reader[T]) extends TaggedReadWriter[T]{ - def findReader(s: String) = if (s == tag) r else null + class Leaf[T](c: ClassTag[_], private[upickle] override val tagKey: String, tagValue: String, r: ObjectWriter[T] with Reader[T]) extends TaggedReadWriter[T]{ + @deprecated("Not used, left for binary compatibility") + def this(c: ClassTag[_], tag: String, r: ObjectWriter[T] with Reader[T]) = this(c, Annotator.defaultTagKey, tag, r) + + def findReader(s: String) = if (s == tagValue) r else null + @deprecated("Not used, left for binary compatibility") def findWriter(v: Any) = { - if (c.runtimeClass.isInstance(v)) (tag -> r) + if (c.runtimeClass.isInstance(v)) tagValue -> r + else null + } + override def findWriterWithKey(v: Any) = { + if (c.runtimeClass.isInstance(v)) (tagKey, tagValue, r) else null } } - class Node[T](rs: TaggedReadWriter[_ <: T]*) extends TaggedReadWriter[T]{ + class Node[T](private[upickle] override val tagKey: String, rs: TaggedReadWriter[_ <: T]*) extends TaggedReadWriter[T]{ + @deprecated("Not used, left for binary compatibility") + def this(rs: TaggedReadWriter[_ <: T]*) = this(Annotator.defaultTagKey, rs:_*) + def findReader(s: String) = scanChildren(rs)(_.findReader(s)).asInstanceOf[Reader[T]] + @deprecated("Not used, left for binary compatibility") def findWriter(v: Any) = scanChildren(rs)(_.findWriter(v)).asInstanceOf[(String, ObjectWriter[T])] + override def findWriterWithKey(v: Any) = + scanChildren(rs)(_.findWriterWithKey(v)).asInstanceOf[(String, String, ObjectWriter[T])] } } @@ -216,7 +270,13 @@ trait Types{ types => def taggedObjectContext[T](taggedReader: TaggedReader[T], index: Int): ObjVisitor[Any, T] = throw new Abort(taggedExpectedMsg) - def taggedWrite[T, R](w: ObjectWriter[T], tag: String, out: Visitor[_, R], v: T): R + @deprecated("Not used, left for binary compatibility") + def taggedWrite[T, R](w: ObjectWriter[T], tag: String, out: Visitor[_, R], v: T): R + + // Calling deprecated method to maintain binary compatibility + @annotation.nowarn("msg=deprecated") + def taggedWrite[T, R](w: ObjectWriter[T], tagKey: String, tagValue: String, out: Visitor[_, R], v: T): R = + taggedWrite(w, tagValue, out, v) private[this] def scanChildren[T, V](xs: Seq[T])(f: T => V) = { var x: V = null.asInstanceOf[V] @@ -249,12 +309,31 @@ class CurrentlyDeriving[T] * for `.equals` equality during writes to determine which tag to use. */ trait Annotator { this: Types => + @deprecated("Not used, left for binary compatibility") def annotate[V](rw: Reader[V], n: String): TaggedReader[V] + + // Calling deprecated method to maintain binary compatibility + @annotation.nowarn("msg=deprecated") + def annotate[V](rw: Reader[V], key: String, value: String): TaggedReader[V] = annotate(rw, value) + + @deprecated("Not used, left for binary compatibility") def annotate[V](rw: ObjectWriter[V], n: String, checker: Annotator.Checker): TaggedWriter[V] - def annotate[V](rw: ObjectWriter[V], n: String)(implicit ct: ClassTag[V]): TaggedWriter[V] = - annotate(rw, n, Annotator.Checker.Cls(ct.runtimeClass)) + + // Calling deprecated method to maintain binary compatibility + @annotation.nowarn("msg=deprecated") + def annotate[V](rw: ObjectWriter[V], key: String, value: String, checker: Annotator.Checker): TaggedWriter[V] = + annotate(rw, value, checker) + + def annotate[V](rw: ObjectWriter[V], key: String, value: String)(implicit ct: ClassTag[V]): TaggedWriter[V] = + annotate(rw, key, value, Annotator.Checker.Cls(ct.runtimeClass)) + + @deprecated("Not used, left for binary compatibility") + final def annotate[V](rw: ObjectWriter[V], n: String)(implicit ct: ClassTag[V]): TaggedWriter[V] = + annotate(rw, Annotator.defaultTagKey, n, Annotator.Checker.Cls(ct.runtimeClass)) } object Annotator{ + def defaultTagKey = "$type" + sealed trait Checker object Checker{ case class Cls(c: Class[_]) extends Checker diff --git a/upickle/implicits/src-2/upickle/implicits/internal/Macros.scala b/upickle/implicits/src-2/upickle/implicits/internal/Macros.scala index bb85b35dc..4e064554d 100644 --- a/upickle/implicits/src-2/upickle/implicits/internal/Macros.scala +++ b/upickle/implicits/src-2/upickle/implicits/internal/Macros.scala @@ -4,7 +4,8 @@ import scala.annotation.{nowarn, StaticAnnotation} import scala.language.experimental.macros import compat._ import acyclic.file -import upickle.implicits.key +import upickle.core.Annotator +import upickle.implicits.{MacrosCommon, key} import language.higherKinds import language.existentials @@ -102,8 +103,15 @@ object Macros { annotate(tpe)(wrapObject(mod2)) } + + @deprecated("Not used, left for binary compatibility") def mergeTrait(subtrees: Seq[Tree], subtypes: Seq[Type], targetType: c.Type): Tree + // Calling deprecated method to maintain binary compatibility + @annotation.nowarn("msg=deprecated") + def mergeTrait(tagKey: String, subtrees: Seq[Tree], subtypes: Seq[Type], targetType: c.Type): Tree = + mergeTrait(subtrees, subtypes, targetType) + def derive(tpe: c.Type) = { if (tpe.typeSymbol.asClass.isTrait || (tpe.typeSymbol.asClass.isAbstractClass && !tpe.typeSymbol.isJava)) { val derived = deriveTrait(tpe) @@ -125,11 +133,12 @@ object Macros { "https://com-lihaoyi.github.io/upickle/#ManualSealedTraitPicklers" fail(tpe, msg) }else{ + val tagKey = customKey(clsSymbol).getOrElse(Annotator.defaultTagKey) val subTypes = fleshedOutSubtypes(tpe).toSeq.sortBy(_.typeSymbol.fullName) // println("deriveTrait") val subDerives = subTypes.map(subCls => q"implicitly[${typeclassFor(subCls)}]") // println(Console.GREEN + "subDerives " + Console.RESET + subDrivess) - val merged = mergeTrait(subDerives, subTypes, tpe) + val merged = mergeTrait(tagKey, subDerives, subTypes, tpe) merged } } @@ -203,12 +212,20 @@ object Macros { * representation with a class label. */ def annotate(tpe: c.Type)(derived: c.universe.Tree) = { - val sealedParent = tpe.baseClasses.find(_.asClass.isSealed) - sealedParent.fold(derived) { parent => - - val index = customKey(tpe.typeSymbol).getOrElse(TypeName(tpe.typeSymbol.fullName).decodedName.toString) - - q"${c.prefix}.annotate($derived, $index)" + val sealedParents = tpe.baseClasses.filter(_.asClass.isSealed) + + if (sealedParents.isEmpty) derived + else { + val tagKey = MacrosCommon.tagKeyFromParents( + tpe.typeSymbol.name.toString, + sealedParents, + customKey, + (_: c.Symbol).name.toString, + fail(tpe, _), + ) + val tagValue = customKey(tpe.typeSymbol).getOrElse(TypeName(tpe.typeSymbol.fullName).decodedName.toString) + + q"${c.prefix}.annotate($derived, $tagKey, $tagValue)" } } @@ -329,8 +346,13 @@ object Macros { } """ } - def mergeTrait(subtrees: Seq[Tree], subtypes: Seq[Type], targetType: c.Type): Tree = { - q"${c.prefix}.Reader.merge[$targetType](..$subtrees)" + + @deprecated("Not used, left for binary compatibility") + def mergeTrait(subtrees: Seq[Tree], subtypes: Seq[Type], targetType: c.Type): Tree = + mergeTrait(Annotator.defaultTagKey, subtrees, subtypes, targetType) + + override def mergeTrait(tagKey: String, subtrees: Seq[Tree], subtypes: Seq[Type], targetType: c.Type): Tree = { + q"${c.prefix}.Reader.merge[$targetType]($tagKey, ..$subtrees)" } } @@ -401,7 +423,12 @@ object Macros { } """ } - def mergeTrait(subtree: Seq[Tree], subtypes: Seq[Type], targetType: c.Type): Tree = { + + @deprecated("Not used, left for binary compatibility") + def mergeTrait(subtrees: Seq[Tree], subtypes: Seq[Type], targetType: c.Type): Tree = + mergeTrait(Annotator.defaultTagKey, subtrees, subtypes, targetType) + + override def mergeTrait(tagKey: String, subtree: Seq[Tree], subtypes: Seq[Type], targetType: c.Type): Tree = { q"${c.prefix}.Writer.merge[$targetType](..$subtree)" } } diff --git a/upickle/implicits/src-3/upickle/implicits/Readers.scala b/upickle/implicits/src-3/upickle/implicits/Readers.scala index b51f98c94..d749e2a0f 100644 --- a/upickle/implicits/src-3/upickle/implicits/Readers.scala +++ b/upickle/implicits/src-3/upickle/implicits/Readers.scala @@ -76,9 +76,9 @@ trait ReadersVersionSpecific } inline if macros.isSingleton[T] then - annotate[T](SingletonReader[T](macros.getSingleton[T]), macros.tagName[T]) + annotate[T](SingletonReader[T](macros.getSingleton[T]), macros.tagKey[T], macros.tagName[T]) else if macros.isMemberOfSealedHierarchy[T] then - annotate[T](reader, macros.tagName[T]) + annotate[T](reader, macros.tagKey[T], macros.tagName[T]) else reader case m: Mirror.SumOf[T] => @@ -87,7 +87,7 @@ trait ReadersVersionSpecific .toList .asInstanceOf[List[Reader[_ <: T]]] - Reader.merge[T](readers: _*) + Reader.merge[T](macros.tagKey[T], readers: _*) } inline def macroRAll[T](using m: Mirror.Of[T]): Reader[T] = inline m match { @@ -99,8 +99,9 @@ trait ReadersVersionSpecific inline given superTypeReader[T: Mirror.ProductOf, V >: T : Reader : Mirror.SumOf] (using NotGiven[CurrentlyDeriving[V]]): Reader[T] = { val actual = implicitly[Reader[V]].asInstanceOf[TaggedReader[T]] + val tagKey = macros.tagKey[T] val tagName = macros.tagName[T] - new TaggedReader.Leaf(tagName, actual.findReader(tagName)) + new TaggedReader.Leaf(tagKey, tagName, actual.findReader(tagName)) } // see comment in MacroImplicits as to why Dotty's extension methods aren't used here diff --git a/upickle/implicits/src-3/upickle/implicits/Writers.scala b/upickle/implicits/src-3/upickle/implicits/Writers.scala index 4638af0aa..fb9d066d2 100644 --- a/upickle/implicits/src-3/upickle/implicits/Writers.scala +++ b/upickle/implicits/src-3/upickle/implicits/Writers.scala @@ -44,9 +44,19 @@ trait WritersVersionSpecific } inline if macros.isSingleton[T] then - annotate[T](SingletonWriter[T](null.asInstanceOf[T]), macros.tagName[T], Annotator.Checker.Val(macros.getSingleton[T])) + annotate[T]( + SingletonWriter[T](null.asInstanceOf[T]), + macros.tagKey[T], + macros.tagName[T], + Annotator.Checker.Val(macros.getSingleton[T]), + ) else if macros.isMemberOfSealedHierarchy[T] then - annotate[T](writer, macros.tagName[T], Annotator.Checker.Cls(implicitly[ClassTag[T]].runtimeClass)) + annotate[T]( + writer, + macros.tagKey[T], + macros.tagName[T], + Annotator.Checker.Cls(implicitly[ClassTag[T]].runtimeClass), + ) else writer case _: Mirror.SumOf[T] => diff --git a/upickle/implicits/src-3/upickle/implicits/macros.scala b/upickle/implicits/src-3/upickle/implicits/macros.scala index 85d4411fa..814eb4274 100644 --- a/upickle/implicits/src-3/upickle/implicits/macros.scala +++ b/upickle/implicits/src-3/upickle/implicits/macros.scala @@ -2,7 +2,7 @@ package upickle.implicits.macros import scala.quoted.{ given, _ } import deriving._, compiletime._ -import upickle.implicits.ReadersVersionSpecific +import upickle.implicits.{MacrosCommon, ReadersVersionSpecific} type IsInt[A <: Int] = A def getDefaultParamsImpl0[T](using Quotes, Type[T]): Map[String, Expr[AnyRef]] = @@ -172,13 +172,30 @@ def writeSnippetsImpl[R, T, WS <: Tuple](thisOuter: Expr[upickle.core.Types with '{()} ) +private def sealedHierarchyParents[T](using Quotes, Type[T]): List[quotes.reflect.Symbol] = + import quotes.reflect._ + + TypeRepr.of[T].baseClasses.filter(_.flags.is(Flags.Sealed)) + inline def isMemberOfSealedHierarchy[T]: Boolean = ${ isMemberOfSealedHierarchyImpl[T] } def isMemberOfSealedHierarchyImpl[T](using Quotes, Type[T]): Expr[Boolean] = - import quotes.reflect._ + Expr(sealedHierarchyParents[T].nonEmpty) - val parents = TypeRepr.of[T].baseClasses +inline def tagKey[T]: String = ${ tagKeyImpl[T] } +def tagKeyImpl[T](using Quotes, Type[T]): Expr[String] = + import quotes.reflect._ - Expr(parents.exists { p => p.flags.is(Flags.Sealed) }) + // `case object`s extend from `Mirror`, which is `sealed` and will never have a `@key` annotation + // so we need to filter it out to ensure it doesn't trigger an error in `tagKeyFromParents` + val mirrorType = Symbol.requiredClass("scala.deriving.Mirror") + + Expr(MacrosCommon.tagKeyFromParents( + Type.show[T], + sealedHierarchyParents[T].filterNot(_ == mirrorType), + extractKey, + (_: Symbol).name, + report.errorAndAbort, + )) inline def tagName[T]: String = ${ tagNameImpl[T] } def tagNameImpl[T](using Quotes, Type[T]): Expr[String] = diff --git a/upickle/implicits/src/upickle/implicits/MacrosCommon.scala b/upickle/implicits/src/upickle/implicits/MacrosCommon.scala index bf4cf0ca2..4f2b1adf2 100644 --- a/upickle/implicits/src/upickle/implicits/MacrosCommon.scala +++ b/upickle/implicits/src/upickle/implicits/MacrosCommon.scala @@ -13,3 +13,30 @@ trait MacrosCommon { } +object MacrosCommon { + def tagKeyFromParents[P]( + typeName: => String, + sealedParents: List[P], + getKey: P => Option[String], + getName: P => String, + fail: String => Nothing, + ): String = + /** + * Valid cases are: + * + * 1. None of the parents have a `@key` annotation + * 2. All of the parents have the same `@key` annotation + */ + sealedParents.flatMap(getKey(_)) match { + case Nil => upickle.core.Annotator.defaultTagKey + case keys @ (key :: _) if keys.length == sealedParents.length && keys.distinct.length == 1 => key + case keys => + fail( + s"Type $typeName inherits from multiple parent types with different discriminator keys:\n\n" + + s" parents: ${sealedParents.map(getName).sorted.mkString(", ")}\n" + + s" keys: ${keys.sorted.mkString(", ")}\n\n" + + "To resolve this, either remove the `@key` annotations from all parents of the type,\n" + + "or make sure all the parents pass the same value to `@key`" + ) + } +} diff --git a/upickle/src/upickle/Api.scala b/upickle/src/upickle/Api.scala index 98773c643..e3d8f5f42 100644 --- a/upickle/src/upickle/Api.scala +++ b/upickle/src/upickle/Api.scala @@ -245,12 +245,21 @@ object default extends AttributeTagged{ */ object legacy extends LegacyApi trait LegacyApi extends Api with Annotator{ - def annotate[V](rw: Reader[V], n: String) = new TaggedReader.Leaf[V](n, rw) - - def annotate[V](rw: ObjectWriter[V], n: String, checker: Annotator.Checker): TaggedWriter[V] = { - new TaggedWriter.Leaf[V](checker, n, rw) + override def annotate[V](rw: Reader[V], key: String, value: String) = { + new TaggedReader.Leaf[V](key, value, rw) } + @deprecated("Not used, left for binary compatibility") + override final def annotate[V](rw: Reader[V], n: String) = + annotate(rw, Annotator.defaultTagKey, n) + + override def annotate[V](rw: ObjectWriter[V], key: String, value: String, checker: Annotator.Checker): TaggedWriter[V] = + new TaggedWriter.Leaf[V](checker, key, value, rw) + + @deprecated("Not used, left for binary compatibility") + override final def annotate[V](rw: ObjectWriter[V], n: String, checker: Annotator.Checker): TaggedWriter[V] = + annotate(rw, Annotator.defaultTagKey, n, checker) + def taggedExpectedMsg = "expected sequence" sealed trait TaggedReaderState object TaggedReaderState{ @@ -287,14 +296,17 @@ trait LegacyApi extends Api with Annotator{ } } - def taggedWrite[T, R](w: ObjectWriter[T], tag: String, out: Visitor[_, R], v: T): R = { + override def taggedWrite[T, R](w: ObjectWriter[T], tagKey: String, tagValue: String, out: Visitor[_, R], v: T): R = { val ctx = out.asInstanceOf[Visitor[Any, R]].visitArray(2, -1) - ctx.visitValue(ctx.subVisitor.visitString(objectTypeKeyWriteMap(tag), -1), -1) + ctx.visitValue(ctx.subVisitor.visitString(objectTypeKeyWriteMap(tagValue), -1), -1) ctx.visitValue(w.write(ctx.subVisitor, v), -1) ctx.visitEnd(-1) } + @deprecated("Not used, left for binary compatibility") + final def taggedWrite[T, R](w: ObjectWriter[T], tag: String, out: Visitor[_, R], v: T): R = + taggedWrite(w, Annotator.defaultTagKey, tag, out, v) } /** @@ -303,19 +315,27 @@ trait LegacyApi extends Api with Annotator{ * of the attribute is. */ trait AttributeTagged extends Api with Annotator{ - def tagName = "$type" - def annotate[V](rw: Reader[V], n: String) = { - new TaggedReader.Leaf[V](n, rw) + @deprecated("Not used, left for binary compatibility") + def tagName = Annotator.defaultTagKey + + override def annotate[V](rw: Reader[V], key: String, value: String) = { + new TaggedReader.Leaf[V](key, value, rw) } + @deprecated("Not used, left for binary compatibility") + override final def annotate[V](rw: Reader[V], n: String) = + annotate(rw, Annotator.defaultTagKey, n) - def annotate[V](rw: ObjectWriter[V], n: String, checker: Annotator.Checker): TaggedWriter[V] = { - new TaggedWriter.Leaf[V](checker, n, rw) + override def annotate[V](rw: ObjectWriter[V], key: String, value: String, checker: Annotator.Checker): TaggedWriter[V] = { + new TaggedWriter.Leaf[V](checker, key, value, rw) } + @deprecated("Not used, left for binary compatibility") + override final def annotate[V](rw: ObjectWriter[V], n: String, checker: Annotator.Checker): TaggedWriter[V] = + annotate(rw, Annotator.defaultTagKey, n, checker) def taggedExpectedMsg = "expected dictionary" - private def isTagName(i: Any) = i match{ - case s: BufferedValue.Str => s.value0.toString == tagName - case s: CharSequence => s.toString == tagName + private def isTagName(tagKey: String, i: Any) = i match{ + case s: BufferedValue.Str => s.value0.toString == tagKey + case s: CharSequence => s.toString == tagKey case _ => false } override def taggedObjectContext[T](taggedReader: TaggedReader[T], index: Int) = { @@ -333,7 +353,7 @@ trait AttributeTagged extends Api with Annotator{ def visitKeyValue(s: Any): Unit = { if (context != null) context.visitKeyValue(s) else { - if (isTagName(s)) () //do nothing + if (isTagName(taggedReader.tagKey, s)) () //do nothing else { // otherwise, go slow path val slowCtx = BufferedValue.Builder.visitObject(-1, true, index).narrow @@ -359,12 +379,12 @@ trait AttributeTagged extends Api with Annotator{ } } def visitEnd(index: Int) = { - def missingKeyMsg = s"""Missing key "$tagName" for tagged dictionary""" + def missingKeyMsg = s"""Missing key "${taggedReader.tagKey}" for tagged dictionary""" if (context == null) throw new Abort(missingKeyMsg) else if (fastPath) context.visitEnd(index).asInstanceOf[T] else{ val x = context.visitEnd(index).asInstanceOf[BufferedValue.Obj] - val keyAttr = x.value0.find(t => isTagName(t._1)) + val keyAttr = x.value0.find(t => isTagName(taggedReader.tagKey, t._1)) .getOrElse(throw new Abort(missingKeyMsg)) ._2 val key = keyAttr.asInstanceOf[BufferedValue.Str].value0.toString @@ -376,7 +396,7 @@ trait AttributeTagged extends Api with Annotator{ for (p <- x.value0) { val (k0, v) = p val k = k0 - if (!isTagName(k)){ + if (!isTagName(taggedReader.tagKey, k)){ val keyVisitor = ctx2.visitKey(-1) ctx2.visitKeyValue(BufferedValue.transform(k, keyVisitor)) @@ -389,18 +409,21 @@ trait AttributeTagged extends Api with Annotator{ } } - def taggedWrite[T, R](w: ObjectWriter[T], tag: String, out: Visitor[_, R], v: T): R = { + override def taggedWrite[T, R](w: ObjectWriter[T], tagKey: String, tagValue: String, out: Visitor[_, R], v: T): R = { - if (w.isInstanceOf[SingletonWriter[_]]) out.visitString(tag, -1) + if (w.isInstanceOf[SingletonWriter[_]]) out.visitString(tagValue, -1) else { val ctx = out.asInstanceOf[Visitor[Any, R]].visitObject(w.length(v) + 1, true, -1) val keyVisitor = ctx.visitKey(-1) - ctx.visitKeyValue(keyVisitor.visitString(tagName, -1)) - ctx.visitValue(ctx.subVisitor.visitString(objectTypeKeyWriteMap(tag), -1), -1) + ctx.visitKeyValue(keyVisitor.visitString(tagKey, -1)) + ctx.visitValue(ctx.subVisitor.visitString(objectTypeKeyWriteMap(tagValue), -1), -1) w.writeToObject(ctx, v) val res = ctx.visitEnd(-1) res } } + @deprecated("Not used, left for binary compatibility") + final def taggedWrite[T, R](w: ObjectWriter[T], tag: String, out: Visitor[_, R], v: T): R = + taggedWrite(w, Annotator.defaultTagKey, tag, out, v) } diff --git a/upickle/test/src/upickle/MacroTests.scala b/upickle/test/src/upickle/MacroTests.scala index a9b2706c5..02b8077ed 100644 --- a/upickle/test/src/upickle/MacroTests.scala +++ b/upickle/test/src/upickle/MacroTests.scala @@ -13,6 +13,31 @@ object KeyedPerson { implicit val rw: RW[KeyedPerson] = upickle.default.macroRW } +@upickle.implicits.key("customKey") +sealed trait KeyedADT +object KeyedADT { + implicit val rw: RW[KeyedADT] = upickle.default.macroRW + + case object Foo extends KeyedADT { + implicit val rw: RW[Foo.type] = upickle.default.macroRW + } + case class Bar(i: Int) extends KeyedADT + object Bar { + implicit val rw: RW[Bar] = upickle.default.macroRW + } +} + +@upickle.implicits.key("customKey1") +sealed trait MultiKeyedADT1 +@upickle.implicits.key("customKey2") +sealed trait MultiKeyedADT2 +case object MultiKeyedObj extends MultiKeyedADT1 with MultiKeyedADT2 + +@upickle.implicits.key("customKey") +sealed trait SomeMultiKeyedADT1 +sealed trait SomeMultiKeyedADT2 +case object SomeMultiKeyedObj extends SomeMultiKeyedADT1 with SomeMultiKeyedADT2 + object Custom { trait ThingBase{ val i: Int @@ -49,7 +74,7 @@ sealed trait TypedFoo object TypedFoo{ import upickle.default._ implicit val readWriter: ReadWriter[TypedFoo] = ReadWriter.merge( - macroRW[Bar], macroRW[Baz], macroRW[Quz] + macroRW[Bar], macroRW[Baz], macroRW[Quz], ) case class Bar(i: Int) extends TypedFoo @@ -636,5 +661,23 @@ object MacroTests extends TestSuite { UnknownKeys.DisallowPickler.read[UnknownKeys.DisAllow]("""{"id":1, "name":"x", "omg": "wtf"}""") } } + + test("keyedADT") { + val fooJson = "\"upickle.KeyedADT.Foo\"" + upickle.default.read[KeyedADT](fooJson) ==> KeyedADT.Foo + upickle.default.write[KeyedADT](KeyedADT.Foo) ==> fooJson + + val barJson = """{"customKey":"upickle.KeyedADT.Bar","i":1}""" + upickle.default.read[KeyedADT](barJson) ==> KeyedADT.Bar(1) + upickle.default.write[KeyedADT](KeyedADT.Bar(1)) ==> barJson + } + + test("multiKeyedADT") { + compileError("upickle.default.macroRW[upickle.MultiKeyedObj.type]") + .check("", "inherits from multiple parent types with different discriminator keys") + + compileError("upickle.default.macroRW[upickle.SomeMultiKeyedObj.type]") + .check("", "inherits from multiple parent types with different discriminator keys") + } } } diff --git a/upickle/test/src/upickle/example/ExampleTests.scala b/upickle/test/src/upickle/example/ExampleTests.scala index 0ed02796d..fc8306c5b 100644 --- a/upickle/test/src/upickle/example/ExampleTests.scala +++ b/upickle/test/src/upickle/example/ExampleTests.scala @@ -66,6 +66,14 @@ object KeyedTag{ } case object C extends A } +object KeyedTagKey { + @upickle.implicits.key("_tag") + sealed trait Tag + case class ATag(i: Int) extends Tag + object ATag { + implicit val rw: RW[ATag] = macroRW + } +} object Custom2{ class CustomThing2(val i: Int, val s: String) object CustomThing2 { @@ -87,6 +95,7 @@ object Generic { import KeyedTag._ +import KeyedTagKey._ import Keyed._ import Sealed._ import Simple._ @@ -312,6 +321,13 @@ object ExampleTests extends TestSuite { write(B(10)) ==> """{"$type":"Bee","i":10}""" read[B]("""{"$type":"Bee","i":10}""") ==> B(10) } + test("tagKey"){ + write(ATag(11)) ==> + """{"_tag":"upickle.example.KeyedTagKey.ATag","i":11}""" + + read[ATag]("""{"_tag":"upickle.example.KeyedTagKey.ATag","i":11}""") ==> + ATag(11) + } test("snakeCase"){ object SnakePickle extends upickle.AttributeTagged{ def camelToSnake(s: String) = { diff --git a/upickleReadme/Readme.scalatex b/upickleReadme/Readme.scalatex index 630a7755c..fc3b7aecc 100644 --- a/upickleReadme/Readme.scalatex +++ b/upickleReadme/Readme.scalatex @@ -410,6 +410,18 @@ you try to tackle the resource issue (bandwidth, storage, CPU) because FQNs might get quite long + @p + You can also use @hl.scala{@@key} to change the key used when pickling a + member of a sealed hierarchy. The default key is @hl.scala{$type}, as seen + above, but you can change it by annotating the @hl.scala{sealed trait}: + + @hl.ref(exampleTests, Seq("object KeyedTagKey", "")) + @hl.ref(exampleTests, Seq("\"keyed\"", "\"tagKey\"", "")) + + @p + This is useful in cases where you need to override uPickle's default + behavior, for example to preserve compatibility with another JSON library. + @sect{JSON Dictionary Formats} @p By default, serializing a @hl.scala{Map[K, V]} generates a nested