Core module of circe, a JSON library for Scala that enables developers to encode and decode JSON data with type safety and functional programming principles.
—
Circe provides comprehensive error handling with detailed failure information for both parsing and decoding operations. The error system tracks operation history and provides clear error messages.
Base class for all circe errors.
sealed abstract class Error extends Exception {
override def fillInStackTrace(): Throwable = this
}Represents JSON parsing failures from malformed JSON strings.
final case class ParsingFailure(message: String, underlying: Throwable) extends Error {
override def getMessage: String = message
override def toString(): String = s"ParsingFailure: $message"
}object ParsingFailure {
implicit val eqParsingFailure: Eq[ParsingFailure]
implicit val showParsingFailure: Show[ParsingFailure]
}Represents JSON decoding failures with detailed path and error information.
sealed abstract class DecodingFailure extends Error {
// Error information
def message: String
def history: List[CursorOp]
def reason: DecodingFailure.Reason
def pathToRootString: Option[String]
// Message creation
override def getMessage: String
override def toString: String
// Error manipulation
def copy(message: String = message, history: => List[CursorOp] = history): DecodingFailure
def withMessage(message: String): DecodingFailure
def withReason(reason: DecodingFailure.Reason): DecodingFailure
}object DecodingFailure {
// Constructors
def apply(message: String, ops: => List[CursorOp]): DecodingFailure
def apply(reason: Reason, ops: => List[CursorOp]): DecodingFailure
def apply(reason: Reason, cursor: ACursor): DecodingFailure
def fromThrowable(t: Throwable, ops: => List[CursorOp]): DecodingFailure
// Pattern matching support
def unapply(error: Error): Option[(String, List[CursorOp])]
// Type class instances
implicit val eqDecodingFailure: Eq[DecodingFailure]
implicit val showDecodingFailure: Show[DecodingFailure]
// Failure reasons
sealed abstract class Reason
object Reason {
case object MissingField extends Reason
case class WrongTypeExpectation(expectedJsonFieldType: String, jsonValue: Json) extends Reason
case class CustomReason(message: String) extends Reason
}
}Convenience exception for aggregating multiple errors.
final case class Errors(errors: NonEmptyList[Error]) extends Exception {
def toList: List[Error]
override def fillInStackTrace(): Throwable = this
}object Error {
implicit val eqError: Eq[Error]
implicit val showError: Show[Error]
}import io.circe._
import io.circe.parser._
// Invalid JSON string
val malformedJson = """{"name": "John", "age":}"""
parse(malformedJson) match {
case Left(ParsingFailure(message, underlying)) =>
println(s"Parsing failed: $message")
println(s"Underlying cause: ${underlying.getMessage}")
case Right(json) =>
println("Parsing succeeded")
}
// Result: ParsingFailure: expected json value got '}' (line 1, column 24)import io.circe._
import io.circe.parser._
case class Person(name: String, age: Int, email: String)
implicit val personDecoder: Decoder[Person] = Decoder.forProduct3("name", "age", "email")(Person.apply)
val json = parse("""{"name": "John", "age": "thirty"}""").getOrElse(Json.Null)
json.as[Person] match {
case Left(DecodingFailure(message, history)) =>
println(s"Decoding failed: $message")
println(s"Path: ${CursorOp.opsToPath(history)}")
println(s"History: $history")
case Right(person) =>
println(s"Decoded: $person")
}
// Result:
// Decoding failed: Got value '"thirty"' with wrong type, expecting number
// Path: .age
// History: List(DownField(age))import io.circe._
import io.circe.parser._
case class Person(name: String, age: Int, email: String)
implicit val personDecoder: Decoder[Person] = Decoder.forProduct3("name", "age", "email")(Person.apply)
val json = parse("""{"name": "John", "age": 30}""").getOrElse(Json.Null)
json.as[Person] match {
case Left(failure @ DecodingFailure(message, history)) =>
println(s"Missing field error: $message")
println(s"Reason: ${failure.reason}")
failure.reason match {
case DecodingFailure.Reason.MissingField =>
println("Field is missing from JSON")
case DecodingFailure.Reason.WrongTypeExpectation(expected, actual) =>
println(s"Expected $expected but got ${actual.name}")
case DecodingFailure.Reason.CustomReason(msg) =>
println(s"Custom error: $msg")
}
case Right(person) =>
println(s"Decoded: $person")
}import io.circe._
implicit val positiveIntDecoder: Decoder[Int] = Decoder[Int]
.ensure(_ > 0, "Age must be positive")
.withErrorMessage("Invalid age value")
val negativeAge = Json.fromInt(-5)
negativeAge.as[Int] match {
case Left(failure) =>
println(failure.message) // "Invalid age value"
case Right(age) =>
println(s"Age: $age")
}import io.circe._
case class Email(value: String)
implicit val emailDecoder: Decoder[Email] = Decoder[String].emap { str =>
if (str.contains("@") && str.contains("."))
Right(Email(str))
else
Left("Invalid email format")
}
val invalidEmail = Json.fromString("notanemail")
invalidEmail.as[Email] match {
case Left(failure) =>
println(s"Validation failed: ${failure.message}")
case Right(email) =>
println(s"Valid email: ${email.value}")
}import io.circe._
import io.circe.parser._
import cats.data.ValidatedNel
import cats.syntax.apply._
case class Person(name: String, age: Int, email: String)
implicit val nameDecoder: Decoder[String] = Decoder[String].ensure(_.nonEmpty, "Name cannot be empty")
implicit val ageDecoder: Decoder[Int] = Decoder[Int].ensure(_ > 0, "Age must be positive")
implicit val emailDecoder: Decoder[String] = Decoder[String].emap { str =>
if (str.contains("@")) Right(str) else Left("Invalid email")
}
implicit val personDecoder: Decoder[Person] = new Decoder[Person] {
def apply(c: HCursor): Decoder.Result[Person] = {
(
c.downField("name").as[String](nameDecoder),
c.downField("age").as[Int](ageDecoder),
c.downField("email").as[String](emailDecoder)
).mapN(Person.apply)
}
override def decodeAccumulating(c: HCursor): Decoder.AccumulatingResult[Person] = {
(
c.downField("name").as[String](nameDecoder).toValidatedNel,
c.downField("age").as[Int](ageDecoder).toValidatedNel,
c.downField("email").as[String](emailDecoder).toValidatedNel
).mapN(Person.apply)
}
}
val badJson = parse("""{"name": "", "age": -5, "email": "invalid"}""").getOrElse(Json.Null)
badJson.asAccumulating[Person] match {
case cats.data.Valid(person) =>
println(s"Decoded: $person")
case cats.data.Invalid(errors) =>
println("Multiple errors:")
errors.toList.foreach(error => println(s" - ${error.message}"))
}
// Result:
// Multiple errors:
// - Name cannot be empty
// - Age must be positive
// - Invalid emailimport io.circe._
// Try multiple decoders
implicit val flexibleStringDecoder: Decoder[String] =
Decoder[String] or Decoder[Int].map(_.toString) or Decoder[Boolean].map(_.toString)
// Recover from specific errors
implicit val recoveryDecoder: Decoder[Int] = Decoder[Int].handleErrorWith { failure =>
failure.reason match {
case DecodingFailure.Reason.WrongTypeExpectation(_, json) if json.isString =>
Decoder[String].emap(str =>
try Right(str.toInt)
catch { case _: NumberFormatException => Left("Not a number") }
)
case _ => Decoder.failed(failure)
}
}
val stringNumber = Json.fromString("42")
stringNumber.as[Int](recoveryDecoder) match {
case Right(num) => println(s"Recovered: $num") // "Recovered: 42"
case Left(error) => println(s"Failed: ${error.message}")
}import io.circe._
import io.circe.parser._
val nestedJson = parse("""
{
"users": [
{"profile": {"name": "John", "age": "invalid"}}
]
}
""").getOrElse(Json.Null)
val cursor = nestedJson.hcursor
val result = cursor
.downField("users")
.downN(0)
.downField("profile")
.downField("age")
.as[Int]
result match {
case Left(failure) =>
println(s"Error at path: ${failure.pathToRootString.getOrElse("unknown")}")
println(s"Operation history: ${failure.history}")
println(s"Cursor path: ${CursorOp.opsToPath(failure.history)}")
case Right(age) =>
println(s"Age: $age")
}
// Result:
// Error at path: .users[0].profile.age
// Operation history: List(DownField(users), DownN(0), DownField(profile), DownField(age))
// Cursor path: .users[0].profile.ageimport io.circe._
sealed trait ValidationError
case class TooYoung(age: Int) extends ValidationError
case class InvalidEmail(email: String) extends ValidationError
implicit val ageDecoder: Decoder[Int] = Decoder[Int].emap { age =>
if (age >= 18) Right(age)
else Left(s"Too young: $age")
}
implicit val emailDecoder: Decoder[String] = Decoder[String].ensure(
email => email.contains("@") && email.length > 5,
"Invalid email format"
)
case class User(age: Int, email: String)
implicit val userDecoder: Decoder[User] = Decoder.forProduct2("age", "email")(User.apply)
val json = parse("""{"age": 16, "email": "bad"}""").getOrElse(Json.Null)
json.as[User] match {
case Left(failure) =>
println(s"Validation failed: ${failure.message}")
println(s"At: ${failure.pathToRootString.getOrElse("root")}")
case Right(user) =>
println(s"Valid user: $user")
}import io.circe._
// Decoder that never fails
implicit val safeStringDecoder: Decoder[Option[String]] =
Decoder[String].map(Some(_)).handleErrorWith(_ => Decoder.const(None))
// Either for multiple possible types
implicit val stringOrIntDecoder: Decoder[Either[String, Int]] =
Decoder[String].map(Left(_)) or Decoder[Int].map(Right(_))
val json = Json.fromInt(42)
json.as[Option[String]] match {
case Right(None) => println("Not a string, but that's OK")
case Right(Some(str)) => println(s"String: $str")
case Left(_) => println("This shouldn't happen with safe decoder")
}
json.as[Either[String, Int]] match {
case Right(Left(str)) => println(s"String: $str")
case Right(Right(num)) => println(s"Number: $num")
case Left(error) => println(s"Neither string nor int: ${error.message}")
}Install with Tessl CLI
npx tessl i tessl/maven-io-circe--circe-core