Friday
Friday is built off of a parser that is the topic of a chapter in Function Programming in Scala (2nd Ed) of parser combinators.
The library provides an AST to convert JSON to/from, as well as type-classes for Encoders, Decoders, and Codecs.
There is also an emphasis on Json AST helper methods to easily work with JSON, without having to convert to/from an explicit schema.
Great uses of this library are for encoding/decoding JSON with the Spider http server/client project, or simple JSON driven configuration files.
Working with the AST
The Json AST is described fully as
enum Json {
case JsonNull
case JsonBool(value: Boolean)
case JsonNumber(value: Double)
case JsonString(value: String)
case JsonArray(value: IndexedSeq[Json])
case JsonObject(value: Map[String, Json])
}
A json string can be parsed to Json
, using the parse
method on the Json
companion object.
def parse(json: String): Either[ParseError, Json]
Once you have a reference to a Json
, you can (dangerously) access the underlying value by calling one of
- strVal
- numVal
- boolVal
- arrVal
- objVal
These methods will throw an exception if the underlying value is not the approriate type, for example
JsonString("Some Str").strVal // Works fine
JsonString("Some Str").numVal // Throws exception
You can safely use one of the following methods to get an Option of the underlying value
- strOpt
- numOpt
- boolOpt
- arrOpt
- objOpt
To quickly parse through possible sub-fields on a JsonObject, there is a ?
extension method on both Json
and
Option[Json]
that takes a field name as an argument
def ?(field: String): Option[Json]
With this, if we had some JSON
{
"name": "Branch",
"some": {
"nested": {
"key": "value"
}
}
}
We can do things like
val js: Json = ???
val maybeName: Option[Json] = js ? "name" // It's there!
val deepField: Option[Json] = js ? "some" ? "nested" ? "key" // This too!
// Not present, but doesn't throw an exception attempting to access deeper fields!
val probablyNot: Option[String] = js ? "totally" ? "not" ? "there"
Encoder, Decoders, and Codecs
Friday provides type-classes to convert Json to/from Scala models.
Decoders
For some type A
, we can define a JsonEncoder[A]
that can convert Json
to A
by providing an implementation of
def decode(json: Json): Try[A]
For example, the following decoder can convert Json
to String
given JsonDecoder[String] with {
def decode(json: Json): Try[String] =
Try(json.strVal)
}
Some decoders for common types like this are provided and can accessed by importing
import dev.wishingtree.branch.friday.JsonDecoder.given
Auto derivation is also supported for Product
types (Sum
types soon™️). We can use derives
on case classes as
case class Person(name: String, age: Int)derives JsonDecoder
With the proper decoder in scope, we can decode JSON (or JSON that is still in String form) with
val personJson =
"""
|{
| "name": "Mark",
| "age": 42
|}
|""".stripMargin
println {
Json.decode[Person](personJson) // returns a Try[Person]
}
Encoders
For some type A
, we can define an encoder that can convert A
to Json
by providing an implementation of
def encode(a: A): Json
For example, the following encoder can convert String
to Json
given JsonEncoder[String] with {
def encode(a: String): Json = Json.JsonString(a)
}
Some encoders for common types are provided and can accessed by importing
import dev.wishingtree.branch.friday.JsonEncoder.given
Auto derivation is also supported for Product
types (Sum
types soon™️). We can use derives
on case classes as
case class Person(name: String, age: Int)derives JsonEncoder
With the proper encoder in scope, we can use the extension method provided by the type class, or the method on the Json
companion object to convert to Json
Person("Mark", 42).toJson
Json.encode(Person("Mark", 42))
Codecs
We often want to be able to go both ways, so this library provides a codec which has the ability to encode and decode.
trait JsonCodec[A] extends JsonDecoder[A], JsonEncoder[A]
This also supports auto derivation for Product
types (Sum
types soon™️). We can use derives
on case classes as
case class Person(name: String, age: Int)derives JsonCodec
With both an encoder and decoder, there is also the JsonCodec.apply
method to easily create an instance
def apply[A](using
encoder: JsonEncoder[A],
decoder: JsonDecoder[A]
): JsonCodec[A] =
new JsonCodec[A] {
override def decode(json: Json): Try[A] = decoder.decode(json)
override def encode(a: A): Json = encoder.encode(a)
}
Other Libraries
If you like Friday, you should check out uPickle