JSON Codec

← Main | 한국어 →

API | Examples


Overview

The Sigilaris JSON codec provides a lightweight, library-agnostic abstraction for encoding and decoding Scala data structures to JSON. While hash computation is not a critical concern for JSON (unlike the byte codec), this module is designed to allow easy switching between JSON libraries by introducing a minimal JsonValue ADT as an intermediate representation.

Why Custom JSON Abstraction?

Key Features

Quick Start (30 seconds)

import org.sigilaris.core.codec.json.*
import org.sigilaris.core.codec.json.backend.circe.CirceJsonOps

case class User(name: String, age: Int) derives JsonCodec

val user = User("Alice", 30)

// Encode to JsonValue
val jsonValue = JsonEncoder[User].encode(user)

// Print to JSON string (via Circe backend)
val jsonString = CirceJsonOps.print(jsonValue)

// Parse from JSON string
val parsed = CirceJsonOps.parse(jsonString)
// Decode back to User
parsed.flatMap(JsonDecoder[User].decode(_))
// res0: Either[SigilarisFailure, User] = Right(
//   value = User(name = "Alice", age = 30)
// )

That's it! The codec automatically derives instances and you can switch JSON backends by changing imports.

Documentation

What's Included

Core ADT

JsonValue enum with six cases:

Type Classes

Backend Integration

Automatic Derivation

import org.sigilaris.core.codec.json.*

case class Account(id: String, balance: BigDecimal) derives JsonCodec

sealed trait Status derives JsonCodec
case object Active extends Status
case object Inactive extends Status

case class UserAccount(
  username: String,
  account: Account,
  status: Status
) derives JsonCodec
val ua = UserAccount("alice", Account("acc-1", BigDecimal(100)), Active)
// ua: UserAccount = UserAccount(
//   username = "alice",
//   account = Account(id = "acc-1", balance = 100),
//   status = Active
// )
JsonEncoder[UserAccount].encode(ua)
// res2: JsonValue = JObject(
//   fields = Map(
//     "username" -> JString(value = "alice"),
//     "account" -> JObject(
//       fields = Map(
//         "id" -> JString(value = "acc-1"),
//         "balance" -> JString(value = "100")
//       )
//     ),
//     "status" -> JObject(fields = Map("Active" -> JObject(fields = Map())))
//   )
// )

Configuration

JsonConfig controls encoding/decoding behavior:

import org.sigilaris.core.codec.json.*

val config = JsonConfig(
  fieldNaming = FieldNamingPolicy.SnakeCase,      // firstName → first_name
  dropNullValues = true,                          // omit null fields
  treatAbsentAsNull = true,                       // missing → null for Option
  writeBigIntAsString = true,                     // BigInt → "123"
  writeBigDecimalAsString = false,                // BigDecimal → 123.45
  discriminator = DiscriminatorConfig(
    TypeNameStrategy.SimpleName                   // { "TypeName": {...} }
  )
)

Field Naming Policies

Discriminator Strategy

Coproducts (sealed traits) use wrapped-by-type-key encoding:

sealed trait Color
case object Red extends Color
case object Blue extends Color

// Encoded as: { "Red": {} } or { "Blue": {} }

Type name strategies:

Design Philosophy

Separation of Concerns

Unlike the byte codec (which is critical for deterministic hashing), JSON encoding:

The architecture ensures:

Minimal Dependencies

The core JsonValue ADT has zero dependencies on external JSON libraries. Backend adapters are separate modules, making it trivial to:

Example: API Response Encoding

import org.sigilaris.core.codec.json.*
import java.time.Instant

case class Product(id: String, name: String, price: BigDecimal)
  derives JsonCodec

case class ApiResponse(
  data: Product,
  timestamp: Instant,
  status: String
) derives JsonCodec

val response = ApiResponse(
  data = Product("p-1", "Widget", BigDecimal("29.99")),
  timestamp = Instant.parse("2025-01-15T10:30:00Z"),
  status = "success"
)
val json = JsonEncoder[ApiResponse].encode(response)
// json: JsonValue = JObject(
//   fields = Map(
//     "data" -> JObject(
//       fields = Map(
//         "id" -> JString(value = "p-1"),
//         "name" -> JString(value = "Widget"),
//         "price" -> JString(value = "29.99")
//       )
//     ),
//     "timestamp" -> JString(value = "2025-01-15T10:30:00Z"),
//     "status" -> JString(value = "success")
//   )
// )

Next Steps

  1. API Reference: Learn about contramap, emap, and combinators
  2. Examples: See configuration options and advanced patterns

Limitations and Scope

Performance Characteristics


← Main | 한국어 →

API | Examples