JSON serialization is a fundamental concern in any web application, and the Scala ecosystem offers several mature, type-safe approaches that go far beyond simple string manipulation. Unlike dynamic languages where JSON is trivially a hash map, Scala’s type system demands compile-time safety and principled encoding/decoding. In this article, we compare four of the most popular Scala JSON libraries — Circe, Play-JSON, Spray-JSON, and Argonaut — examining their APIs, type-safety guarantees, performance characteristics, and integration with popular frameworks.
Quick Comparison
| Feature | Circe | Play-JSON | Spray-JSON | Argonaut |
|---|---|---|---|---|
| GitHub Stars | ~2.5K | Part of Play | ~1K | ~500 |
| Encoding Model | Type class (automatic/semi-auto) | Type class (Reads/Writes/Format) | Type class (JsonFormat) | Codec-based |
| Derivation | Automatic, semi-auto, manual | Manual macros (JSON.format) | Manual (jsonFormatN) | Manual codecs |
| Shapeless/HList | Yes (core design) | No | No | No |
| Cats/Cats-Effect | Native | Separate module | No | Via argonaut-cats |
| Performance | Very good | Good | Good | Moderate |
| Error Handling | DecodingFailure (accumulating) | JsError (accumulating) | DeserializationException | Cursor history |
| JSON AST | io.circe.Json | play.api.libs.json.JsValue | spray.json.JsValue | argonaut.Json |
| Scala 3 Support | Yes | Yes | Community fork | Limited |
Circe: Type-Class-Driven Purity
Circe (pronounced “SUR-see”) is the current gold standard for JSON in Scala. Built on top of the cats and shapeless libraries, it uses type classes to automatically derive JSON encoders and decoders for case classes at compile time. The name is a pun on Circe the sorceress — it’s “compiler-verified JSON codecs.”
Setup
| |
Basic Usage
| |
Semi-Automatic Derivation
For better compile times and explicit control, use semi-automatic derivation:
| |
Optics (Cursor-Based Manipulation)
Circe provides optics for navigating and modifying JSON without decoding to case classes:
| |
Error Accumulation
Circe’s DecodingFailure accumulates errors so you see all problems at once:
| |
Pros and Cons
Pros:
- Compile-time derivation with zero runtime reflection
- Excellent error messages with accumulating failures
- Optics library for JSON manipulation without case classes
- Deep integration with the Scala type-level programming ecosystem (cats, shapeless)
- Best Scala 3 support of any JSON library
Cons:
- Depends on shapeless which increases compilation time
- Learning curve for developers new to type classes and implicit resolution
- Generic auto-derivation can cause implicit resolution conflicts in large projects
Play-JSON: The Framework Standard
Play-JSON originated as part of the Play Framework but is now a standalone library. It uses a Reads[T] / Writes[T] / Format[T] type class pattern and is the default choice for any Play Framework application.
Setup
| |
Basic Usage
| |
Custom Validation
Play-JSON’s Reads allows inline validation during parsing:
| |
JsonTransformers
Play-JSON provides a powerful transformation DSL for updating nested JSON structures:
| |
Pros and Cons
Pros:
- First-class integration with Play Framework
Readscombinators allow validation during parsing- JSON transformer API for complex JSON manipulation
- Mature, battle-tested in production since 2012
Cons:
- Combinator-based format syntax becomes verbose for large case classes
- Macro derivation (
Json.format) has limited customization - Less ergonomic outside of Play Framework projects
- No built-in accumulating errors (manual collection required)
Spray-JSON: The Lightweight Contender
Spray-JSON is a minimalist JSON library developed alongside the Spray HTTP toolkit (predecessor to Akka HTTP). It requires you to define JsonFormat instances for each type but compensates with simplicity and zero dependencies beyond the Scala standard library.
Setup
| |
Basic Usage
| |
Handling Optional and Nested Types
| |
Custom Formats
Spray-JSON makes custom formats straightforward:
| |
Pros and Cons
Pros:
- No dependencies beyond Scala stdlib
- Simple, predictable API with minimal magic
jsonFormatNhelpers for case classes up to 22 fields- Fast compilation with minimal implicit resolution overhead
Cons:
- No support for Scala 3 (community forks available)
- Limited error messages (no cursor path in
DeserializationException) - No accumulating errors — stops at the first failure
- Manual format definition required for complex types
Argonaut: The Pioneer
Argonaut pioneered purely functional JSON handling in Scala. While its influence on the ecosystem is immense (Circe was directly inspired by it), the project has slowed in maintenance. It remains a solid choice if you need its specific features or are working with an existing Argonaut-based codebase.
Setup
| |
Basic Usage
| |
Cursor-Based Navigation
Argonaut’s cursor provides detailed navigation with history tracking:
| |
Pros and Cons
Pros:
- Pioneered functional JSON in Scala — deeply influential design
- Excellent cursor API with full navigation history
- Decent type-class derivation for standard case classes
- Integration with scalaz (argonaut-scalaz module)
Cons:
- Dormant maintenance (last release in 2023)
- No official Scala 3 support
- Slower than Circe for large JSON documents
- Smaller community and fewer learning resources
Performance and Ecosystem Integration
For JSON-intensive applications, performance matters. Circe consistently leads in benchmarks due to its use of Jawn (a fast JSON parser) and compile-time codec generation. Spray-JSON is also fast for simple cases but defers to runtime reflection for jsonFormatN helpers. Play-JSON’s transformer API introduces overhead for complex transformations, making it best suited for request/response handling in Play applications rather than batch JSON processing.
| Scenario | Recommendation |
|---|---|
| New Scala 3 project | Circe |
| Play Framework application | Play-JSON |
| Minimal dependencies | Spray-JSON |
| Akka HTTP / Pekko HTTP | Spray-JSON (native integration) |
| Cats/Cats-Effect stack | Circe |
| Legacy Argonaut codebase | Argonaut (or migrate to Circe) |
For a broader look at Scala development tools, see our Scala testing frameworks guide and our JVM build tools comparison.
FAQ
Which library has the best Scala 3 support?
Circe has the most robust Scala 3 support. The core library compiles under Scala 3, and circe-generic uses Scala 3’s native derives keyword for automatic derivation: case class User(id: Long, name: String) derives Codec.AsObject. Play-JSON also supports Scala 3, while Spray-JSON and Argonaut rely on community forks.
How do I handle camelCase to snake_case conversion?
Circe provides Configuration.default.withSnakeCaseMemberNames via circe-generic-extras. Play-JSON requires manual (JsPath \ "field_name").format[T] mapping. Spray-JSON needs a custom jsonFormatN with explicit field names. Argonaut’s casecodecN accepts explicit field name parameters.
Can these libraries handle streaming JSON (JSON Lines, NDJSON)?
Yes. Circe has circe-fs2 and circe-iteratee modules for streaming. Play-JSON integrates with Akka Streams for JSON streaming. Spray-JSON can parse NDJSON line by line using the standard parseJson per line. For large datasets, consider using a dedicated streaming JSON parser like jawn-ast or zio-json.
What about ADT (sealed trait) serialization?
All four libraries support sealed trait hierarchies, but the approach differs. Circe uses circe-generic-extras and discriminators: @JsonCodec sealed trait Event; case class Click(x: Int, y: Int) extends Event. Play-JSON requires custom Reads/Writes with pattern matching. Spray-JSON uses jsonFormat with a discriminator field. This is the area where Circe’s type-class approach provides the most ergonomic developer experience.
Is it worth migrating from Argonaut to Circe?
If you’re starting a new project, choose Circe. For existing Argonaut codebases, migration is worthwhile if you need Scala 3 support, better performance, or active maintenance. The migration path is well-documented: both libraries use the CodecJson pattern, and Circe’s API was directly inspired by Argonaut. Typical migration effort for a medium codebase is 1-2 days.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com