API Reference
← JSON Codec | API | Examples
Overview
This document provides detailed API reference for the JSON codec type classes: JsonEncoder
, JsonDecoder
, JsonCodec
, JsonKeyCodec
, and configuration types.
JsonValue
The core JSON AST - a minimal, library-agnostic representation of JSON values.
Cases
enum JsonValue:
case JNull
case JBool(value: Boolean)
case JNumber(value: BigDecimal)
case JString(value: String)
case JArray(values: Vector[JsonValue])
case JObject(fields: Map[String, JsonValue])
Constructors
import org.sigilaris.core.codec.json.*
import JsonValue.*
// Object constructor
obj("name" -> JString("Alice"), "age" -> JNumber(30))
// res0: JsonValue = JObject(
// fields = Map("name" -> JString(value = "Alice"), "age" -> JNumber(value = 30))
// )
// Array constructor
arr(JNumber(1), JNumber(2), JNumber(3))
// res1: JsonValue = JArray(
// values = Vector(JNumber(value = 1), JNumber(value = 2), JNumber(value = 3))
// )
// Null alias
nullValue
// res2: JsonValue = JNull
JsonEncoder
JsonEncoder[A]
is a contravariant type class for encoding Scala values to JsonValue
.
Core Methods
encode
def encode(value: A): JsonValue
Encodes a value to JSON AST.
Example:
val encoder = JsonEncoder[Int]
encoder.encode(42)
// res3: JsonValue = JNumber(value = 42)
Combinators
contramap
def contramap[B](f: B => A): JsonEncoder[B]
Creates a new encoder by applying a function before encoding (contravariant functor).
Example:
import org.sigilaris.core.codec.json.*
case class UserId(value: String)
given JsonEncoder[UserId] = JsonEncoder[String].contramap(_.value)
JsonEncoder[UserId].encode(UserId("user-123"))
// res5: JsonValue = JString(value = "user-123")
Use Case: Transform custom types to encodable types.
JsonDecoder
JsonDecoder[A]
is a covariant type class for decoding JsonValue
to Scala values.
Core Methods
decode
def decode(json: JsonValue): Either[DecodeFailure, A]
Decodes JSON to a value, returning either a failure or the decoded value.
Example:
import org.sigilaris.core.failure.DecodeFailure
val decoder = JsonDecoder[Int]
val json = JsonValue.JNumber(42)
decoder.decode(json)
// res6: Either[DecodeFailure, Int] = Right(value = 42)
Combinators
map
def map[B](f: A => B): JsonDecoder[B]
Transforms the decoded value (covariant functor).
Example:
import org.sigilaris.core.codec.json.*
case class UserId(value: String)
given JsonDecoder[UserId] = JsonDecoder[String].map(UserId(_))
val json = JsonValue.JString("user-123")
// json: JsonValue = JString(value = "user-123")
JsonDecoder[UserId].decode(json)
// res8: Either[DecodeFailure, UserId] = Right(
// value = UserId(value = "user-123")
// )
emap
def emap[B](f: A => Either[DecodeFailure, B]): JsonDecoder[B]
Transforms the decoded value with validation.
Example:
import org.sigilaris.core.codec.json.*
import org.sigilaris.core.failure.DecodeFailure
import cats.syntax.either.*
case class PositiveInt(value: Int)
given JsonDecoder[PositiveInt] = JsonDecoder[Int].emap { n =>
if n > 0 then PositiveInt(n).asRight
else DecodeFailure(s"Value $n must be positive").asLeft
}
val validJson = JsonValue.JNumber(10)
// validJson: JsonValue = JNumber(value = 10)
val invalidJson = JsonValue.JNumber(-5)
// invalidJson: JsonValue = JNumber(value = -5)
JsonDecoder[PositiveInt].decode(validJson)
// res10: Either[DecodeFailure, PositiveInt] = Right(
// value = PositiveInt(value = 10)
// )
JsonDecoder[PositiveInt].decode(invalidJson).isLeft
// res11: Boolean = true
Use Case: Add business rule validation during decoding.
JsonCodec
JsonCodec[A]
combines both JsonEncoder[A]
and JsonDecoder[A]
.
trait JsonCodec[A] extends JsonEncoder[A] with JsonDecoder[A]
Usage
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)
// res13: Either[DecodeFailure, Person] = Right(
// value = Person(name = "Alice", age = 30)
// )
Automatic Derivation
import org.sigilaris.core.codec.json.*
// Products (case classes)
case class Address(street: String, city: String) derives JsonCodec
// Coproducts (sealed traits)
sealed trait Status derives JsonCodec
case object Active extends Status
case object Inactive extends Status
// Nested
case class User(name: String, address: Address, status: Status)
derives JsonCodec
val user = User("Bob", Address("Main St", "NYC"), Active)
// user: User = User(
// name = "Bob",
// address = Address(street = "Main St", city = "NYC"),
// status = Active
// )
JsonEncoder[User].encode(user)
// res15: JsonValue = JObject(
// fields = Map(
// "name" -> JString(value = "Bob"),
// "address" -> JObject(
// fields = Map(
// "street" -> JString(value = "Main St"),
// "city" -> JString(value = "NYC")
// )
// ),
// "status" -> JObject(fields = Map("Active" -> JObject(fields = Map())))
// )
// )
JsonKeyCodec
JsonKeyCodec[A]
handles encoding/decoding of map keys (strings in JSON objects).
Core Methods
trait JsonKeyCodec[A]:
def encode(value: A): String
def decode(key: String): Either[DecodeFailure, A]
Built-in Instances
String
: Identity encodingInt
,Long
,BigInt
: Numeric string representationUUID
: Standard UUID string format
Example:
import org.sigilaris.core.codec.json.*
import java.util.UUID
val map = Map(
UUID.fromString("550e8400-e29b-41d4-a716-446655440000") -> "value1",
UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8") -> "value2"
)
val json = JsonEncoder[Map[UUID, String]].encode(map)
// json: JsonValue = JObject(
// fields = Map(
// "550e8400-e29b-41d4-a716-446655440000" -> JString(value = "value1"),
// "6ba7b810-9dad-11d1-80b4-00c04fd430c8" -> JString(value = "value2")
// )
// )
Map keys are encoded as JSON object field names (strings).
JsonConfig
Configuration controlling encode/decode behavior.
Fields
case class JsonConfig(
fieldNaming: FieldNamingPolicy,
dropNullValues: Boolean,
treatAbsentAsNull: Boolean,
writeBigIntAsString: Boolean,
writeBigDecimalAsString: Boolean,
discriminator: DiscriminatorConfig
)
Default Configuration
import org.sigilaris.core.codec.json.*
JsonConfig.default
// res18: JsonConfig = JsonConfig(
// fieldNaming = Identity,
// dropNullValues = true,
// treatAbsentAsNull = true,
// writeBigIntAsString = true,
// writeBigDecimalAsString = true,
// discriminator = DiscriminatorConfig(typeNameStrategy = SimpleName)
// )
Field Naming Policies
enum FieldNamingPolicy:
case Identity // Keep as-is
case SnakeCase // firstName → first_name
case KebabCase // firstName → first-name
case CamelCase // FirstName → firstName
Example:
import org.sigilaris.core.codec.json.*
case class UserProfile(firstName: String, lastName: String)
derives JsonCodec
val profile = UserProfile("Alice", "Smith")
// Identity (default)
JsonEncoder[UserProfile].encode(profile)
// res20: JsonValue = JObject(
// fields = Map(
// "firstName" -> JString(value = "Alice"),
// "lastName" -> JString(value = "Smith")
// )
// )
// Snake case
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)
// )
// Note: Encoder uses global config; pass config explicitly in production
Null Handling
dropNullValues
If true
, null values are omitted from encoded objects.
import org.sigilaris.core.codec.json.*
case class OptionalData(required: String, optional: Option[String])
derives JsonCodec
val withNull = OptionalData("value", None)
// withNull: OptionalData = OptionalData(required = "value", optional = None)
JsonEncoder[OptionalData].encode(withNull)
// res22: JsonValue = JObject(
// fields = Map("required" -> JString(value = "value"))
// )
// With dropNullValues=true, "optional" field would be omitted
treatAbsentAsNull
If true
, missing fields decode as null
for Option[A]
.
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")))
// With treatAbsentAsNull=true, age decodes as None
JsonDecoder[PartialData].decode(json)
// res24: Either[DecodeFailure, PartialData] = Right(
// value = PartialData(name = "Alice", age = None)
// )
Number Formatting
writeBigIntAsString / writeBigDecimalAsString
Controls whether big numbers are encoded as JSON strings or numbers.
import org.sigilaris.core.codec.json.*
case class Amounts(bigInt: BigInt, bigDecimal: BigDecimal)
derives JsonCodec
val amounts = Amounts(BigInt("123456789012345678901234567890"), BigDecimal("123.456"))
// amounts: Amounts = Amounts(
// bigInt = 123456789012345678901234567890,
// bigDecimal = 123.456
// )
JsonEncoder[Amounts].encode(amounts)
// res26: JsonValue = JObject(
// fields = Map(
// "bigInt" -> JString(value = "123456789012345678901234567890"),
// "bigDecimal" -> JString(value = "123.456")
// )
// )
// With writeBigIntAsString=true: { "bigInt": "123456789012345678901234567890", ... }
// Decoders accept both string and number representations
Discriminator Configuration
Controls coproduct (sealed trait) encoding strategy.
case class DiscriminatorConfig(
typeNameStrategy: TypeNameStrategy
)
enum TypeNameStrategy:
case SimpleName // Use case class/object name
case FullyQualified // Use full package path
case Custom(mapping: Map[String, String]) // Custom mapping
Example:
import org.sigilaris.core.codec.json.*
sealed trait Event derives JsonCodec
case class UserCreated(userId: String) extends Event
case class UserDeleted(userId: String) extends Event
val event: Event = UserCreated("user-1")
// event: Event = UserCreated(userId = "user-1")
JsonEncoder[Event].encode(event)
// res28: JsonValue = JObject(
// fields = Map(
// "UserCreated" -> JObject(fields = Map("userId" -> JString(value = "user-1")))
// )
// )
// Encoded as: { "UserCreated": { "userId": "user-1" } }
Custom Type Names:
import org.sigilaris.core.codec.json.*
val customConfig = JsonConfig.default.copy(
discriminator = DiscriminatorConfig(
TypeNameStrategy.Custom(Map(
"UserCreated" -> "user.created",
"UserDeleted" -> "user.deleted"
))
)
)
// Would encode as: { "user.created": { "userId": "user-1" } }
JsonParser and JsonPrinter
Backend-agnostic interfaces for string ↔ JsonValue
conversion.
trait JsonParser[BackendJson]:
def parse(input: String): Either[ParseFailure, JsonValue]
trait JsonPrinter[BackendJson]:
def print(json: JsonValue): String
Circe Backend
import org.sigilaris.core.codec.json.*
import org.sigilaris.core.codec.json.backend.circe.CirceJsonOps
val jsonString = """{"name":"Alice","age":30}"""
// Parse string → 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 → string
parsed.map(CirceJsonOps.print)
// res31: Either[ParseFailure, String] = Right(
// value = "{\"name\":\"Alice\",\"age\":30}"
// )
Error Handling
DecodeFailure
Decoding failures are represented by DecodeFailure
:
case class DecodeFailure(message: String) extends SigilarisFailure
Common failure scenarios:
- Type Mismatch: Expected type doesn't match JSON structure
- Missing Field: Required field absent in JSON object
- Validation Failure: Value decoded but failed
emap
validation - Unknown Subtype: Coproduct discriminator doesn't match known types
Example:
import org.sigilaris.core.codec.json.*
// Type mismatch
JsonDecoder[Int].decode(JsonValue.JString("not a number"))
// res33: Either[DecodeFailure, Int] = Left(
// value = DecodeFailure(msg = "Expected number, got JString")
// )
// Missing field
case class Required(name: String) derives JsonCodec
JsonDecoder[Required].decode(JsonValue.obj())
// res34: Either[DecodeFailure, Required] = Left(
// value = DecodeFailure(msg = "Expected string, got ")
// )
Best Practices
1. Use contramap for Encoders
case class Timestamp(millis: Long)
given JsonEncoder[Timestamp] = JsonEncoder[Long].contramap(_.millis)
2. Use emap for Validation
import org.sigilaris.core.codec.json.*
import org.sigilaris.core.failure.DecodeFailure
import cats.syntax.either.*
case class Email(value: String)
given JsonDecoder[Email] = JsonDecoder[String].emap { s =>
if s.contains("@") then Email(s).asRight
else DecodeFailure("Invalid email format").asLeft
}
3. Leverage Automatic Derivation
case class Account(id: String, balance: BigDecimal) derives JsonCodec
// Instances automatically available
4. Custom Configurations
import org.sigilaris.core.codec.json.*
val apiConfig = JsonConfig.default.copy(
fieldNaming = FieldNamingPolicy.SnakeCase,
dropNullValues = true,
writeBigDecimalAsString = false
)
// Use config explicitly when decoding
case class ApiData(userName: String, accountBalance: BigDecimal)
derives JsonCodec
val json = JsonValue.obj(
"user_name" -> JsonValue.JString("alice"),
"account_balance" -> JsonValue.JNumber(100.50)
)
// Pass config via configured givens
// val decs = JsonDecoder.configured(apiConfig); import decs.given; summon[JsonDecoder[ApiData]].decode(json)
Performance Notes
- Encoding: O(n) where n is the size of data structure
- Decoding: O(n) with early exit on failures
- Derivation: Compile-time, no runtime overhead
- Backend Conversion: Minimal AST transformation overhead
See Also
- Examples: Practical usage patterns
← JSON Codec | API | Examples