Skip to main content
Version: 2.x

Configuration

Field naming​

By default, the field names of a case class are used as the JSON fields, but it is easy to override this with an annotation @jsonField.

Moreover, you can also mark a whole case class with a member name transformation that will be applied to all members using @jsonMemberNames annotation. It takes an argument of type JsonMemberFormat which encodes the transformation that will be applied to member names.

Four most popular transformations are already provided: KebabCase, SnakeCase, PascalCase and CamelCase. If you require something more specific you can also use CustomCase which takes a function of shape String => String as an argument and can be used to perform any arbitrary transformation. @jsonField annotation takes priority over the transformation defined by @jsonMemberNames.

Here's an example json with most fields snake_cased and one kebab-cased:

{
"passion_fruit": true,
"granny_smith": true,
"dragon_fruit": true,
"blood-orange": false
}

And here's the target case class:

import zio.json._

@jsonMemberNames(SnakeCase)
case class FruitBasket(
passionFruit: Boolean,
grannySmith: Boolean,
dragonFruit: Boolean,
@jsonField("blood-orange") bloodOrange: Boolean
)

Notice that all fields are camelCased in Scala and will be both encoded and decoded correctly to snake_case in JSON except bloodOrange field that is annotated with a @jsonField override that will force it to become "blood-orange" after serialization.

It is also possible to change the type hint that is used to discriminate case classes with @jsonHint.

For example, these annotations change the expected JSON of our Fruit family

import zio.json._

sealed trait Fruit

@jsonHint("banaani") case class Banana(
@jsonField("bendiness") curvature: Double
) extends Fruit

@jsonHint("omena") case class Apple(
@jsonField("bad") poison: Boolean
) extends Fruit

object Fruit {
implicit val encoder: JsonEncoder[Fruit] =
DeriveJsonEncoder.gen[Fruit]
}

val banana: Fruit = Banana(0.5)
// banana: Fruit = Banana(curvature = 0.5)
val apple: Fruit = Apple(false)
// apple: Fruit = Apple(poison = false)

from

{"Banana":{"curvature":0.5}}
{"Apple":{"poison":false}}

to

banana.toJson
// res1: String = {"banaani":{"bendiness":0.5}}
apple.toJson
// res2: String = {"omena":{"bad":false}}

Another way of changing type hint is using @jsonHintNames annotation on sealed class. It allows to apply transformation to all type hint values in hierarchy. Same transformations are provided as for @jsonMemberNames annotation.

Here's an example:

import zio.json._

@jsonHintNames(SnakeCase)
sealed trait FruitKind

case class GoodFruit(good: Boolean) extends FruitKind

case class BadFruit(bad: Boolean) extends FruitKind

object FruitKind {
implicit val encoder: JsonEncoder[FruitKind] =
DeriveJsonEncoder.gen[FruitKind]
}

val goodFruit: FruitKind = GoodFruit(true)
// goodFruit: FruitKind = GoodFruit(good = true)
val badFruit: FruitKind = BadFruit(true)
// badFruit: FruitKind = BadFruit(bad = true)

goodFruit.toJson
// res3: String = "{\"good_fruit\":{\"good\":true}}"
badFruit.toJson
// res4: String = "{\"bad_fruit\":{\"bad\":true}}"

jsonDiscriminator​

A popular alternative way to encode sealed traits:

{"type":"banaani", "bendiness":0.5}

{"type":"omena", "bad":false}

is discouraged for performance reasons. However, if we have no choice in the matter, it may be accommodated with the @jsonDiscriminator annotation

@jsonDiscriminator("type") sealed trait Fruit

Extra fields​

We can raise an error if we encounter unexpected fields by using the @jsonNoExtraFields annotation on a case class.

@jsonNoExtraFields case class Watermelon(pips: Int)

object Watermelon {
implicit val decoder: JsonDecoder[Watermelon] =
DeriveJsonDecoder.gen[Watermelon]
}
"""{ "pips": 32 }""".fromJson[Watermelon]
// res6: Either[String, Watermelon] = Right(value = Watermelon(pips = 32))
"""{ "pips": 32, "color": "yellow" }""".fromJson[Watermelon]
// res7: Either[String, Watermelon] = Left(value = "(invalid extra field)")

Aliases​

Since zio-json 0.4.3.

After a case class field has changed name, you may still want to read JSON documents that use the old name. This is supported by the @jsonAliases annotation.

case class Strawberry(
@jsonAliases("seeds") seedCount: Int
)

object Strawberry {
implicit val decoder: JsonDecoder[Strawberry] =
DeriveJsonDecoder.gen[Strawberry]
}

The following two expressions result in an equal value:

"""{ "seeds": 32 }""".fromJson[Strawberry]
// res8: Either[String, Strawberry] = Right(value = Strawberry(seedCount = 32))
"""{ "seedCount": 32 }""".fromJson[Strawberry]
// res9: Either[String, Strawberry] = Right(value = Strawberry(seedCount = 32))

The @jsonAliases annotation supports multiple aliases. The annotation has no effect on encoding.

@jsonDerive​

Requires zio-json-macros

@jsonDerive allows to reduce that needs to be written using an annotation macro to generate JsonDecoder/JsonEncoder at build-time.

For generating both Encoder and Decoder, simply use jsonDerive

For example:

import zio.json._

@jsonDerive case class Watermelon(pips: Int)

It is equivalent to:

import zio.json._

case class Watermelon(pips: Int)

object Watermelon {
implicit val codec: JsonCodec[Watermelon] =
DeriveJsonCodec.gen[Watermelon]
}

To generate only an encoder, we can set it as config parameter:

For example:

import zio.json._

@jsonDerive(JsonDeriveConfig.Encoder) case class Watermelon(pips: Int)

It is equivalent to:

import zio.json._

case class Watermelon(pips: Int)

object Watermelon {
implicit val encoder: JsonEncoder[Watermelon] =
DeriveJsonEncoder.gen[Watermelon]
}

To generate only a decoder, we can set it as config parameter:

For example:

import zio.json._

@jsonDerive(JsonDeriveConfig.Decoder) case class Watermelon(pips: Int)

It is equivalent to:

import zio.json._

case class Watermelon(pips: Int)

object Watermelon {
implicit val decoder: JsonDecoder[Watermelon] =
DeriveJsonDecoder.gen[Watermelon]
}