or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

builtin-types.mdcore-serialization.mdindex.mdjson-integration.mdmessagepack-integration.mdsealed-traits.mdstreaming.mdtype-classes.md
tile.json

sealed-traits.mddocs/

Sealed Traits

Automatic tagging and discrimination for sealed trait hierarchies with customizable behavior. uPickle provides sophisticated support for sealed trait serialization with multiple tagging strategies.

Capabilities

Automatic Sealed Trait Derivation

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)

TaggedReader

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))

TaggedWriter

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]
}

TaggedReadWriter

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 (AttributeTagged)

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

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)

Custom Tag Keys

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])
)

Singleton Object Handling

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"]

Custom Discriminator Logic

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]])
  }
}

Types

/**
 * 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  
  }
}