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

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:

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

See Also


← JSON Codec | API | Examples