CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-circe--circe-core

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.

Pending
Overview
Eval results
Files

error-handling.mddocs/

Error Handling

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.

Error

Base class for all circe errors.

sealed abstract class Error extends Exception {
  override def fillInStackTrace(): Throwable = this
}

ParsingFailure

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"
}

ParsingFailure Companion Object

object ParsingFailure {
  implicit val eqParsingFailure: Eq[ParsingFailure]
  implicit val showParsingFailure: Show[ParsingFailure]
}

DecodingFailure

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
}

DecodingFailure Companion Object

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
  }
}

Errors

Convenience exception for aggregating multiple errors.

final case class Errors(errors: NonEmptyList[Error]) extends Exception {
  def toList: List[Error]
  override def fillInStackTrace(): Throwable = this
}

Error Companion Object

object Error {
  implicit val eqError: Eq[Error]
  implicit val showError: Show[Error]
}

Usage Examples

Parsing Errors

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)

Decoding Errors

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))

Missing Field Errors

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")
}

Custom Error Messages

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")
}

Validation Errors

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}")
}

Accumulating Errors

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 email

Error Recovery

import 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}")
}

Path Information

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.age

Custom Error Types

import 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")
}

Working with Either and Option

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

docs

cursor-navigation.md

error-handling.md

index.md

json-data-types.md

json-printing.md

key-encoding-decoding.md

type-classes.md

tile.json