Examples
← JSON Codec | API | Examples
Overview
This document demonstrates practical usage patterns for the JSON codec, including configuration options, custom encoders/decoders, and real-world scenarios.
Basic Product Encoding
import org.sigilaris.core.codec.json.*
case class Person(name: String, age: Int) derives JsonCodec
val person = Person("Alice", 30)
// Encode
val json = JsonEncoder[Person].encode(person)
// json: JsonValue = JObject(
// fields = Map("name" -> JString(value = "Alice"), "age" -> JNumber(value = 30))
// )
// Decode
JsonDecoder[Person].decode(json)
// res0: Either[DecodeFailure, Person] = Right(
// value = Person(name = "Alice", age = 30)
// )
Nested Structures
import org.sigilaris.core.codec.json.*
case class Address(street: String, city: String, zipCode: String)
derives JsonCodec
case class Company(name: String, address: Address)
derives JsonCodec
case class Employee(
id: String,
name: String,
company: Company,
salary: BigDecimal
) derives JsonCodec
val employee = Employee(
id = "emp-001",
name = "Bob Smith",
company = Company("Acme Inc", Address("123 Main St", "NYC", "10001")),
salary = BigDecimal("75000.00")
)
// employee: Employee = Employee(
// id = "emp-001",
// name = "Bob Smith",
// company = Company(
// name = "Acme Inc",
// address = Address(street = "123 Main St", city = "NYC", zipCode = "10001")
// ),
// salary = 75000.00
// )
JsonEncoder[Employee].encode(employee)
// res2: JsonValue = JObject(
// fields = Map(
// "id" -> JString(value = "emp-001"),
// "name" -> JString(value = "Bob Smith"),
// "company" -> JObject(
// fields = Map(
// "name" -> JString(value = "Acme Inc"),
// "address" -> JObject(
// fields = Map(
// "street" -> JString(value = "123 Main St"),
// "city" -> JString(value = "NYC"),
// "zipCode" -> JString(value = "10001")
// )
// )
// )
// ),
// "salary" -> JString(value = "75000.00")
// )
// )
Coproduct (Sealed Trait) Encoding
Sealed traits use wrapped-by-type-key discriminator:
import org.sigilaris.core.codec.json.*
sealed trait PaymentMethod derives JsonCodec
case class CreditCard(number: String, expiry: String) extends PaymentMethod
case class BankTransfer(accountNumber: String, routingNumber: String)
extends PaymentMethod
case object Cash extends PaymentMethod
val payment1: PaymentMethod = CreditCard("1234-5678", "12/25")
// payment1: PaymentMethod = CreditCard(number = "1234-5678", expiry = "12/25")
val payment2: PaymentMethod = Cash
// payment2: PaymentMethod = Cash
// Wrapped discriminator format
JsonEncoder[PaymentMethod].encode(payment1)
// res4: JsonValue = JObject(
// fields = Map(
// "CreditCard" -> JObject(
// fields = Map(
// "number" -> JString(value = "1234-5678"),
// "expiry" -> JString(value = "12/25")
// )
// )
// )
// )
JsonEncoder[PaymentMethod].encode(payment2)
// res5: JsonValue = JObject(fields = Map("Cash" -> JObject(fields = Map())))
Field Naming Policies
Snake Case
import org.sigilaris.core.codec.json.*
case class UserProfile(firstName: String, lastName: String, emailAddress: String)
derives JsonCodec
val profile = UserProfile("Alice", "Smith", "alice@example.com")
// Default: Identity
JsonEncoder[UserProfile].encode(profile)
// res7: JsonValue = JObject(
// fields = Map(
// "firstName" -> JString(value = "Alice"),
// "lastName" -> JString(value = "Smith"),
// "emailAddress" -> JString(value = "alice@example.com")
// )
// )
// To use SnakeCase, create encoder with config
// (In practice, you'd configure globally or via given instances)
val snakeConfig = JsonConfig.default.copy(
fieldNaming = FieldNamingPolicy.SnakeCase
)
// snakeConfig: JsonConfig = JsonConfig(
// fieldNaming = SnakeCase,
// dropNullValues = true,
// treatAbsentAsNull = true,
// writeBigIntAsString = true,
// writeBigDecimalAsString = true,
// discriminator = DiscriminatorConfig(typeNameStrategy = SimpleName)
// )
// With snakeConfig: { "first_name": "Alice", "last_name": "Smith", ... }
Kebab Case
import org.sigilaris.core.codec.json.*
val kebabConfig = JsonConfig.default.copy(
fieldNaming = FieldNamingPolicy.KebabCase
)
// firstName → first-name
Camel Case
import org.sigilaris.core.codec.json.*
case class DataClass(FirstName: String, LastName: String)
derives JsonCodec
val camelConfig = JsonConfig.default.copy(
fieldNaming = FieldNamingPolicy.CamelCase
)
// FirstName → firstName
Optional Fields and Null Handling
import org.sigilaris.core.codec.json.*
case class UserData(
name: String,
email: Option[String],
phone: Option[String]
) derives JsonCodec
val user1 = UserData("Alice", Some("alice@example.com"), None)
// user1: UserData = UserData(
// name = "Alice",
// email = Some(value = "alice@example.com"),
// phone = None
// )
val user2 = UserData("Bob", None, Some("555-1234"))
// user2: UserData = UserData(
// name = "Bob",
// email = None,
// phone = Some(value = "555-1234")
// )
JsonEncoder[UserData].encode(user1)
// res11: JsonValue = JObject(
// fields = Map(
// "name" -> JString(value = "Alice"),
// "email" -> JString(value = "alice@example.com")
// )
// )
JsonEncoder[UserData].encode(user2)
// res12: JsonValue = JObject(
// fields = Map(
// "name" -> JString(value = "Bob"),
// "phone" -> JString(value = "555-1234")
// )
// )
Drop Null Values
import org.sigilaris.core.codec.json.*
val dropNullConfig = JsonConfig.default.copy(dropNullValues = true)
// With this config, None fields would be omitted from output
Treat Absent as Null
import org.sigilaris.core.codec.json.*
case class PartialData(name: String, age: Option[Int]) derives JsonCodec
// Missing "age" field
val json = JsonValue.obj("name" -> JsonValue.JString("Alice"))
// json: JsonValue = JObject(fields = Map("name" -> JString(value = "Alice")))
val treatAbsentConfig = JsonConfig.default.copy(treatAbsentAsNull = true)
// treatAbsentConfig: JsonConfig = JsonConfig(
// fieldNaming = Identity,
// dropNullValues = true,
// treatAbsentAsNull = true,
// writeBigIntAsString = true,
// writeBigDecimalAsString = true,
// discriminator = DiscriminatorConfig(typeNameStrategy = SimpleName)
// )
// With this config, missing "age" decodes as None
val decs = JsonDecoder.configured(treatAbsentConfig)
// decs: Decoders = org.sigilaris.core.codec.json.JsonDecoder$configured$Decoders@17eb0d2c
import decs.given
JsonDecoder[PartialData].decode(json)
// res15: Either[DecodeFailure, PartialData] = Right(
// value = PartialData(name = "Alice", age = None)
// )
Big Number Formatting
BigInt as String
import org.sigilaris.core.codec.json.*
case class Transaction(
id: String,
amount: BigInt,
nonce: BigInt
) derives JsonCodec
val tx = Transaction(
"tx-001",
BigInt("123456789012345678901234567890"),
BigInt("42")
)
// tx: Transaction = Transaction(
// id = "tx-001",
// amount = 123456789012345678901234567890,
// nonce = 42
// )
// Default: writeBigIntAsString = true
JsonEncoder[Transaction].encode(tx)
// res17: JsonValue = JObject(
// fields = Map(
// "id" -> JString(value = "tx-001"),
// "amount" -> JString(value = "123456789012345678901234567890"),
// "nonce" -> JString(value = "42")
// )
// )
// Encodes as: { ..., "amount": "123456789012345678901234567890", ... }
BigDecimal as Number
import org.sigilaris.core.codec.json.*
case class Price(currency: String, amount: BigDecimal) derives JsonCodec
val price = Price("USD", BigDecimal("29.99"))
// price: Price = Price(currency = "USD", amount = 29.99)
// Default: writeBigDecimalAsString = true
JsonEncoder[Price].encode(price)
// res19: JsonValue = JObject(
// fields = Map(
// "currency" -> JString(value = "USD"),
// "amount" -> JString(value = "29.99")
// )
// )
// To encode as JSON number: writeBigDecimalAsString = false
Decoders accept both string and number representations for robustness.
Collections
Lists
import org.sigilaris.core.codec.json.*
case class Playlist(name: String, songs: List[String]) derives JsonCodec
val playlist = Playlist("Favorites", List("Song A", "Song B", "Song C"))
// playlist: Playlist = Playlist(
// name = "Favorites",
// songs = List("Song A", "Song B", "Song C")
// )
JsonEncoder[Playlist].encode(playlist)
// res21: JsonValue = JObject(
// fields = Map(
// "name" -> JString(value = "Favorites"),
// "songs" -> JArray(
// values = Vector(
// JString(value = "Song A"),
// JString(value = "Song B"),
// JString(value = "Song C")
// )
// )
// )
// )
Vectors
import org.sigilaris.core.codec.json.*
case class Tags(values: Vector[String]) derives JsonCodec
val tags = Tags(Vector("scala", "json", "codec"))
// tags: Tags = Tags(values = Vector("scala", "json", "codec"))
JsonEncoder[Tags].encode(tags)
// res23: JsonValue = JObject(
// fields = Map(
// "values" -> JArray(
// values = Vector(
// JString(value = "scala"),
// JString(value = "json"),
// JString(value = "codec")
// )
// )
// )
// )
Maps with String Keys
import org.sigilaris.core.codec.json.*
case class Config(settings: Map[String, String]) derives JsonCodec
val config = Config(Map("theme" -> "dark", "language" -> "en"))
// config: Config = Config(
// settings = Map("theme" -> "dark", "language" -> "en")
// )
JsonEncoder[Config].encode(config)
// res25: JsonValue = JObject(
// fields = Map(
// "settings" -> JObject(
// fields = Map(
// "theme" -> JString(value = "dark"),
// "language" -> JString(value = "en")
// )
// )
// )
// )
Maps with Non-String Keys
Requires JsonKeyCodec[K]
instance:
import org.sigilaris.core.codec.json.*
import java.util.UUID
case class UserPermissions(permissions: Map[UUID, String])
derives JsonCodec
val permissions = UserPermissions(Map(
UUID.fromString("550e8400-e29b-41d4-a716-446655440000") -> "admin",
UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8") -> "user"
))
// permissions: UserPermissions = UserPermissions(
// permissions = Map(
// 550e8400-e29b-41d4-a716-446655440000 -> "admin",
// 6ba7b810-9dad-11d1-80b4-00c04fd430c8 -> "user"
// )
// )
JsonEncoder[UserPermissions].encode(permissions)
// res27: JsonValue = JObject(
// fields = Map(
// "permissions" -> JObject(
// fields = Map(
// "550e8400-e29b-41d4-a716-446655440000" -> JString(value = "admin"),
// "6ba7b810-9dad-11d1-80b4-00c04fd430c8" -> JString(value = "user")
// )
// )
// )
// )
Map keys are encoded as JSON object field names (strings).
Custom Encoders and Decoders
Using contramap (Encoder)
import org.sigilaris.core.codec.json.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter
case class CustomDate(value: LocalDate)
given JsonEncoder[CustomDate] =
JsonEncoder[String].contramap { cd =>
cd.value.format(DateTimeFormatter.ISO_LOCAL_DATE)
}
JsonEncoder[CustomDate].encode(CustomDate(LocalDate.of(2025, 1, 15)))
// res29: JsonValue = JString(value = "2025-01-15")
Using map and emap (Decoder)
import org.sigilaris.core.codec.json.*
import org.sigilaris.core.failure.DecodeFailure
import cats.syntax.either.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import scala.util.Try
case class CustomDate(value: LocalDate)
given JsonDecoder[CustomDate] =
JsonDecoder[String].emap { s =>
Try(LocalDate.parse(s, DateTimeFormatter.ISO_LOCAL_DATE))
.toEither
.bimap(
ex => DecodeFailure(s"Invalid date format: ${ex.getMessage}"),
CustomDate(_)
)
}
val validDate = JsonValue.JString("2025-01-15")
// validDate: JsonValue = JString(value = "2025-01-15")
val invalidDate = JsonValue.JString("not-a-date")
// invalidDate: JsonValue = JString(value = "not-a-date")
JsonDecoder[CustomDate].decode(validDate)
// res31: Either[DecodeFailure, CustomDate] = Right(
// value = CustomDate(value = 2025-01-15)
// )
JsonDecoder[CustomDate].decode(invalidDate).isLeft
// res32: Boolean = true
Bidirectional Custom Codec
import org.sigilaris.core.codec.json.*
import org.sigilaris.core.failure.DecodeFailure
import cats.syntax.either.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import scala.util.Try
case class CustomDate(value: LocalDate)
object CustomDate:
given JsonEncoder[CustomDate] with
def encode(cd: CustomDate): JsonValue =
JsonValue.JString(cd.value.format(DateTimeFormatter.ISO_LOCAL_DATE))
given JsonDecoder[CustomDate] = new JsonDecoder[CustomDate]:
def decode(json: JsonValue): Either[DecodeFailure, CustomDate] =
json match
case JsonValue.JString(s) =>
Try(LocalDate.parse(s, DateTimeFormatter.ISO_LOCAL_DATE))
.toEither
.bimap(
ex => DecodeFailure(s"Invalid date: ${ex.getMessage}"),
CustomDate(_)
)
case _ =>
DecodeFailure(s"Expected JString for CustomDate, got $json").asLeft
given JsonCodec[CustomDate] = JsonCodec(summon[JsonEncoder[CustomDate]], summon[JsonDecoder[CustomDate]])
val date = CustomDate(LocalDate.of(2025, 1, 15))
// date: CustomDate = CustomDate(value = 2025-01-15)
val encoded = JsonEncoder[CustomDate].encode(date)
// encoded: JsonValue = JString(value = "2025-01-15")
JsonDecoder[CustomDate].decode(encoded)
// res34: Either[DecodeFailure, CustomDate] = Right(
// value = CustomDate(value = 2025-01-15)
// )
Custom Discriminator Mapping
import org.sigilaris.core.codec.json.*
sealed trait Event derives JsonCodec
case class UserCreated(userId: String, username: String) extends Event
case class UserDeleted(userId: String) extends Event
case class UserUpdated(userId: String, fields: Map[String, String]) extends Event
val event1: Event = UserCreated("user-1", "alice")
// event1: Event = UserCreated(userId = "user-1", username = "alice")
val event2: Event = UserDeleted("user-2")
// event2: Event = UserDeleted(userId = "user-2")
// Default: SimpleName discriminator
JsonEncoder[Event].encode(event1)
// res36: JsonValue = JObject(
// fields = Map(
// "UserCreated" -> JObject(
// fields = Map(
// "userId" -> JString(value = "user-1"),
// "username" -> JString(value = "alice")
// )
// )
// )
// )
JsonEncoder[Event].encode(event2)
// res37: JsonValue = JObject(
// fields = Map(
// "UserDeleted" -> JObject(fields = Map("userId" -> JString(value = "user-2")))
// )
// )
// Custom mapping
val customConfig = JsonConfig.default.copy(
discriminator = DiscriminatorConfig(
TypeNameStrategy.Custom(Map(
"UserCreated" -> "user.created",
"UserDeleted" -> "user.deleted",
"UserUpdated" -> "user.updated"
))
)
)
// customConfig: JsonConfig = JsonConfig(
// fieldNaming = Identity,
// dropNullValues = true,
// treatAbsentAsNull = true,
// writeBigIntAsString = true,
// writeBigDecimalAsString = true,
// discriminator = DiscriminatorConfig(
// typeNameStrategy = Custom(
// mapping = Map(
// "UserCreated" -> "user.created",
// "UserDeleted" -> "user.deleted",
// "UserUpdated" -> "user.updated"
// )
// )
// )
// )
// With customConfig: { "user.created": { "userId": "user-1", ... } }
Working with JsonParser and JsonPrinter
Parse JSON String
import org.sigilaris.core.codec.json.*
import org.sigilaris.core.codec.json.backend.circe.CirceJsonOps
val jsonString = """{"name":"Alice","age":30}"""
// Parse to JsonValue
val parsed = CirceJsonOps.parse(jsonString)
// parsed: Either[ParseFailure, JsonValue] = Right(
// value = JObject(
// fields = Map(
// "name" -> JString(value = "Alice"),
// "age" -> JNumber(value = 30)
// )
// )
// )
Print JsonValue to String
import org.sigilaris.core.codec.json.*
import org.sigilaris.core.codec.json.backend.circe.CirceJsonOps
val json = JsonValue.obj(
"name" -> JsonValue.JString("Bob"),
"age" -> JsonValue.JNumber(25)
)
// Print to JSON string
CirceJsonOps.print(json)
// res40: String = "{\"name\":\"Bob\",\"age\":25}"
Full Roundtrip
import org.sigilaris.core.codec.json.*
import org.sigilaris.core.codec.json.backend.circe.CirceJsonOps
case class Product(id: String, name: String, price: BigDecimal)
derives JsonCodec
val product = Product("p-1", "Widget", BigDecimal("19.99"))
// Domain → JsonValue
val jsonValue = JsonEncoder[Product].encode(product)
// jsonValue: JsonValue = JObject(
// fields = Map(
// "id" -> JString(value = "p-1"),
// "name" -> JString(value = "Widget"),
// "price" -> JString(value = "19.99")
// )
// )
// JsonValue → String (via Circe)
val jsonString = CirceJsonOps.print(jsonValue)
// jsonString: String = "{\"id\":\"p-1\",\"name\":\"Widget\",\"price\":\"19.99\"}"
// String → JsonValue (via Circe)
val reparsed = CirceJsonOps.parse(jsonString)
// reparsed: Either[ParseFailure, JsonValue] = Right(
// value = JObject(
// fields = Map(
// "id" -> JString(value = "p-1"),
// "name" -> JString(value = "Widget"),
// "price" -> JString(value = "19.99")
// )
// )
// )
// JsonValue → Domain
reparsed.flatMap(JsonDecoder[Product].decode(_))
// res42: Either[SigilarisFailure, Product] = Right(
// value = Product(id = "p-1", name = "Widget", price = 19.99)
// )
Validation with emap
import org.sigilaris.core.codec.json.*
import org.sigilaris.core.failure.DecodeFailure
import cats.syntax.either.*
case class Email(value: String)
object Email:
given JsonEncoder[Email] with
def encode(email: Email): JsonValue =
JsonValue.JString(email.value)
given JsonDecoder[Email] = new JsonDecoder[Email]:
def decode(json: JsonValue): Either[DecodeFailure, Email] =
json match
case JsonValue.JString(s) =>
if s.contains("@") && s.contains(".") then
Email(s).asRight
else
DecodeFailure(s"Invalid email format: $s").asLeft
case _ =>
DecodeFailure(s"Expected string for Email, got $json").asLeft
given JsonCodec[Email] = JsonCodec(summon[JsonEncoder[Email]], summon[JsonDecoder[Email]])
val validEmail = JsonValue.JString("alice@example.com")
// validEmail: JsonValue = JString(value = "alice@example.com")
val invalidEmail = JsonValue.JString("not-an-email")
// invalidEmail: JsonValue = JString(value = "not-an-email")
JsonDecoder[Email].decode(validEmail)
// res44: Either[DecodeFailure, Email] = Right(
// value = Email(value = "alice@example.com")
// )
JsonDecoder[Email].decode(invalidEmail).isLeft
// res45: Boolean = true
Complex Example: API Response
import org.sigilaris.core.codec.json.*
import java.time.Instant
sealed trait ApiStatus derives JsonCodec
case object Success extends ApiStatus
case object Error extends ApiStatus
case class Metadata(
requestId: String,
timestamp: Instant,
status: ApiStatus
) derives JsonCodec
case class UserInfo(
id: String,
username: String,
email: String
) derives JsonCodec
case class ApiResponse(
data: Option[UserInfo],
metadata: Metadata,
errors: List[String]
) derives JsonCodec
val response = ApiResponse(
data = Some(UserInfo("u-1", "alice", "alice@example.com")),
metadata = Metadata(
requestId = "req-123",
timestamp = Instant.parse("2025-01-15T10:30:00Z"),
status = Success
),
errors = List()
)
JsonEncoder[ApiResponse].encode(response)
// res47: JsonValue = JObject(
// fields = Map(
// "data" -> JObject(
// fields = Map(
// "id" -> JString(value = "u-1"),
// "username" -> JString(value = "alice"),
// "email" -> JString(value = "alice@example.com")
// )
// ),
// "metadata" -> JObject(
// fields = Map(
// "requestId" -> JString(value = "req-123"),
// "timestamp" -> JString(value = "2025-01-15T10:30:00Z"),
// "status" -> JObject(fields = Map("Success" -> JObject(fields = Map())))
// )
// ),
// "errors" -> JArray(values = Vector())
// )
// )
← JSON Codec | API | Examples