Automatic tagging and discrimination for sealed trait hierarchies with customizable behavior. uPickle provides sophisticated support for sealed trait serialization with multiple tagging strategies.
Automatic Reader/Writer generation for sealed traits with tagged discrimination.
Usage Examples:
import upickle.default._
// Basic sealed trait hierarchy
sealed trait Animal
case class Dog(name: String, breed: String) extends Animal
case class Cat(name: String, indoor: Boolean) extends Animal
case class Bird(name: String, species: String) extends Animal
// Automatic derivation
implicit val animalRW: ReadWriter[Animal] = ReadWriter.merge(
macroRW[Dog], macroRW[Cat], macroRW[Bird]
)
val animals = List[Animal](
Dog("Buddy", "Golden Retriever"),
Cat("Whiskers", true),
Bird("Tweety", "Canary")
)
// Serialization with automatic tagging
val json = write(animals)
println(json)
// Result: [
// {"$type":"Dog","name":"Buddy","breed":"Golden Retriever"},
// {"$type":"Cat","name":"Whiskers","indoor":true},
// {"$type":"Bird","name":"Tweety","species":"Canary"}
// ]
// Deserialization with automatic discrimination
val parsed = read[List[Animal]](json)Reader with tagging support for sealed trait discrimination during deserialization.
/**
* Reader with tagging support for sealed traits
*/
trait TaggedReader[T] extends SimpleReader[T] {
private[upickle] def tagKey: String
def findReader(s: String): Reader[T]
}
object TaggedReader {
/**
* Leaf reader for a specific tagged type
*/
class Leaf[T](tagKey: String, tagValue: String, shortValue: String, r: Reader[T]) extends TaggedReader[T]
/**
* Node reader that delegates to child readers based on tag
*/
class Node[T](tagKey: String, rs: TaggedReader[_ <: T]*) extends TaggedReader[T]
}Usage Examples:
import upickle.default._
import upickle.core._
sealed trait Status
case object Active extends Status
case object Inactive extends Status
case object Pending extends Status
// Manual tagged reader construction
val statusReader = new TaggedReader.Node[Status](
"$type",
new TaggedReader.Leaf("$type", "Active", "Active", macroR[Active.type]),
new TaggedReader.Leaf("$type", "Inactive", "Inactive", macroR[Inactive.type]),
new TaggedReader.Leaf("$type", "Pending", "Pending", macroR[Pending.type])
)
// Usage
val json = """{"$type":"Active"}"""
val status = statusReader.transform(ujson.parse(json))Writer with tagging support for sealed trait discrimination during serialization.
/**
* Writer with tagging support for sealed traits
*/
trait TaggedWriter[T] extends Writer[T] {
def findWriterWithKey(v: Any): (String, String, ObjectWriter[T])
}
object TaggedWriter {
/**
* Leaf writer for a specific tagged type
*/
class Leaf[T](checker: Annotator.Checker, tagKey: String, tagValue: String, r: ObjectWriter[T]) extends TaggedWriter[T]
/**
* Node writer that delegates to child writers based on runtime type
*/
class Node[T](rs: TaggedWriter[_ <: T]*) extends TaggedWriter[T]
}Combined tagged reader and writer for bidirectional sealed trait handling.
/**
* Combined tagged reader and writer
*/
trait TaggedReadWriter[T] extends ReadWriter[T] with TaggedReader[T] with TaggedWriter[T]
object TaggedReadWriter {
/**
* Leaf readwriter for a specific tagged type
*/
class Leaf[T](c: ClassTag[_], tagKey: String, tagValue: String, r: ObjectWriter[T] with Reader[T]) extends TaggedReadWriter[T]
/**
* Node readwriter that delegates based on tag/type
*/
class Node[T](tagKey: String, rs: TaggedReadWriter[_ <: T]*) extends TaggedReadWriter[T]
}Default tagging strategy using JSON object attributes.
Usage Examples:
import upickle.default._ // Uses AttributeTagged
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(base: Double, height: Double) extends Shape
implicit val shapeRW: ReadWriter[Shape] = macroRW
val shapes = List[Shape](
Circle(5.0),
Rectangle(10.0, 8.0),
Triangle(6.0, 4.0)
)
val json = write(shapes)
println(json)
// Result: [
// {"$type":"Circle","radius":5.0},
// {"$type":"Rectangle","width":10.0,"height":8.0},
// {"$type":"Triangle","base":6.0,"height":4.0}
// ]
// Custom tag key
object CustomApi extends upickle.AttributeTagged {
override def tagName = "kind"
}
import CustomApi._
val customJson = write(shapes)
// Result uses "kind" instead of "$type"Legacy tagging strategy using JSON arrays with type/value pairs.
Usage Examples:
import upickle.legacy._ // Uses LegacyApi
sealed trait Result[T]
case class Success[T](value: T) extends Result[T]
case class Error[T](message: String) extends Result[T]
implicit def resultRW[T: ReadWriter]: ReadWriter[Result[T]] = macroRW
val results = List[Result[String]](
Success("Hello"),
Error("Failed")
)
val json = write(results)
println(json)
// Result: [
// ["Success", {"value":"Hello"}],
// ["Error", {"message":"Failed"}]
// ]
val parsed = read[List[Result[String]]](json)Customizing tag keys for sealed trait discrimination.
Usage Examples:
import upickle.default._
sealed trait EventType
case class UserLogin(userId: String, timestamp: Long) extends EventType
case class UserLogout(userId: String, timestamp: Long) extends EventType
case class DataUpdated(table: String, recordId: String) extends EventType
// Custom API with different tag key
object EventApi extends upickle.AttributeTagged {
override def tagName = "eventType"
}
import EventApi._
implicit val eventTypeRW: ReadWriter[EventType] = macroRW
val events = List[EventType](
UserLogin("user123", System.currentTimeMillis()),
DataUpdated("users", "456")
)
val json = write(events)
// Result uses "eventType" as tag key instead of "$type"
// Multiple tag key inheritance
trait BaseEvent
sealed trait UserEvent extends BaseEvent
sealed trait SystemEvent extends BaseEvent
case class Login(user: String) extends UserEvent
case class Logout(user: String) extends UserEvent
case class Restart(reason: String) extends SystemEvent
implicit val baseEventRW: ReadWriter[BaseEvent] = ReadWriter.merge(
"type", // Custom tag key
ReadWriter.merge[UserEvent](macroRW[Login], macroRW[Logout]),
ReadWriter.merge[SystemEvent](macroRW[Restart])
)Special handling for case objects and singleton values.
Usage Examples:
import upickle.default._
sealed trait Priority
case object Low extends Priority
case object Medium extends Priority
case object High extends Priority
case object Critical extends Priority
implicit val priorityRW: ReadWriter[Priority] = macroRW
// Singleton objects serialize to just the tag
val priority = High
val json = write(priority)
// Result: "High" (string, not object)
val parsed = read[Priority](json)
// Result: High
// Mixed singleton and case class hierarchy
sealed trait Command
case object Start extends Command
case object Stop extends Command
case class Configure(settings: Map[String, String]) extends Command
implicit val commandRW: ReadWriter[Command] = macroRW
val commands = List[Command](Start, Configure(Map("debug" -> "true")), Stop)
val json = write(commands)
// Result: ["Start", {"$type":"Configure","settings":{"debug":"true"}}, "Stop"]Advanced patterns for custom discrimination logic.
Usage Examples:
import upickle.default._
import upickle.core._
// Custom discriminator based on content rather than explicit tag
sealed trait Message
case class TextMessage(content: String) extends Message
case class ImageMessage(url: String, width: Int, height: Int) extends Message
case class VideoMessage(url: String, duration: Int) extends Message
// Custom reader that discriminates based on field presence
implicit val messageReader: Reader[Message] = new TaggedReader[Message] {
override def tagKey = "$type"
def findReader(s: String): Reader[Message] = s match {
case "text" => macroR[TextMessage]
case "image" => macroR[ImageMessage]
case "video" => macroR[VideoMessage]
case _ => null
}
override def visitObject(length: Int, jsonableKeys: Boolean, index: Int) = {
// Custom logic to determine type from content
new ObjVisitor[Any, Message] {
var fields = Map.empty[String, ujson.Value]
def visitKey(index: Int) = upickle.core.StringVisitor
def visitKeyValue(s: Any): Unit = {}
def subVisitor = ujson.Value
def visitValue(v: Any, index: Int): Unit = {
// Store field for analysis
}
def visitEnd(index: Int): Message = {
// Analyze fields to determine type
if (fields.contains("content") && fields.size == 1) {
macroR[TextMessage].visitObject(length, jsonableKeys, index).visitEnd(index)
} else if (fields.contains("width") && fields.contains("height")) {
macroR[ImageMessage].visitObject(length, jsonableKeys, index).visitEnd(index)
} else {
macroR[VideoMessage].visitObject(length, jsonableKeys, index).visitEnd(index)
}
}
}
}
}
// Custom writer with content-based discrimination
implicit val messageWriter: Writer[Message] = new TaggedWriter[Message] {
def findWriterWithKey(v: Any): (String, String, ObjectWriter[Message]) = v match {
case _: TextMessage => ("$type", "text", macroW[TextMessage].asInstanceOf[ObjectWriter[Message]])
case _: ImageMessage => ("$type", "image", macroW[ImageMessage].asInstanceOf[ObjectWriter[Message]])
case _: VideoMessage => ("$type", "video", macroW[VideoMessage].asInstanceOf[ObjectWriter[Message]])
}
}/**
* Configuration for object type key handling
*/
trait Config {
def tagName: String = "$type"
def objectTypeKeyReadMap: Map[String, String] = Map.empty
def objectTypeKeyWriteMap: Map[String, String] = Map.empty
def objectTypeKeyWriteFullyQualified: Boolean = false
}
/**
* Annotation support for tagged serialization
*/
trait Annotator {
def annotate[V](rw: Reader[V], key: String, value: String, shortValue: String): TaggedReader[V]
def annotate[V](rw: ObjectWriter[V], key: String, value: String, shortValue: String, checker: Annotator.Checker): TaggedWriter[V]
}
object Annotator {
def defaultTagKey: String = "$type"
sealed trait Checker
object Checker {
case class Cls(c: Class[_]) extends Checker
case class Val(v: Any) extends Checker
}
}