Automatic argonaut codec derivation with shapeless
It is available for scala 2.12, and 2.13, and depends on argonaut 6.3.
argonaut-shapeless is part of the shapeless ecosystem of Typelevel, and as such endorses the Scala Code of Conduct.
It is one of the very first projects to have used Lazy
from shapeless 2.1,
which made type class derivation with implicits much more robust.
argonaut-shapeless | argonaut | shapeless | refined |
---|---|---|---|
1.3.0 | 6.3.3 | 2.3.x | 0.9.x |
1.2.0-M4 | 6.2-RC2 | 2.3.x | 0.6.x |
1.2.0-M{1,3} | 6.2-M3 | 2.3.x | 0.5.x |
1.1.1 | 6.1a | 2.3.x | 0.4.x |
1.1.0 | 6.1 | 2.3.x | 0.4.x |
1.0.x | 6.1 | 2.2.x | 0.3.5 |
0.3.x | 6.1 | 2.2.x | n/a |
Add to your build.sbt
libraryDependencies +=
"com.github.alexarchambault" %% "argonaut-shapeless_6.3" % "1.3.0"
The examples below assume you imported the content of
argonaut
, argonaut.Argonaut
, and argonaut.ArgonautShapeless
, like
import argonaut._, Argonaut._, ArgonautShapeless._
case class CC(i: Int, s: String)
// encoding
val encode = EncodeJson.of[CC]
val json = encode(CC(2, "a"))
json.nospaces == """{"i":2,"s":"a"}"""
// decoding
val decode = DecodeJson.of[CC]
val result = decode.decodeJson(json)
result == DecodeResult.ok(CC(2, "a"))
The way case classes are encoded can be customized, see below.
import argonaut._, Argonaut._, ArgonautShapeless._
sealed trait Base
case class First(i: Int) extends Base
case class Second(s: String) extends Base
// encoding
val encode = EncodeJson.of[Base]
val json = encode(First(2))
json.nospaces == """{"First":{"i":2}}"""
// decoding
val decode = DecodeJson.of[Base]
val result = decode.decodeJson(json)
result == DecodeResult.ok(First(2))
Like upickle, fields equal to their default value are not put in the result JSON object.
case class CC(i: Int = 4, s: String = "foo")
CC().asJson.nospaces == "{}"
CC(i = 3).asJson.nospaces == """{"i":3}"""
CC(i = 4, s = "baz").asJson.nospaces == """{"s":"baz"}"""
"{}".decodeOption[CC] == Some(CC())
"""{"i":2}""".decodeOption[CC] == Some(CC(i = 2))
"""{"s":"a"}""".decodeOption[CC] == Some(CC(s = "a"))
This can be turned off by providing the alwaysIncludeDefaultValue JsonProductCodecFor
.
implicit def alwaysIncludeCodecFor[T]: derive.JsonProductCodecFor[T] =
derive.JsonProductCodecFor.alwaysIncludeDefaultValue
CC().asJson.nospaces == """{"i":4,"s":"foo"}"""
When encoding / decoding a case class C
, argonaut-shapeless looks
for an implicit JsonProductCodecFor[C]
, which has a field
codec: JsonProductCodec
. A JsonProductCodec
provides a general way of
encoding / decoding case classes.
The default JsonProductCodecFor[T]
for all types provides
JsonProductCodec.obj
, which encodes / decodes case classes as shown above.
This default can be changed, e.g. to convert field names to serpent_case
,
import argonaut.derive._
implicit def serpentCaseCodecFor[T]: JsonProductCodecFor[T] =
JsonProductCodecFor(JsonProductCodec.adapt(_.toSerpentCase))
case class Identity(firstName: String, lastName: String)
Identity("Jacques", "Chirac").asJson.nospaces == """{"first_name":"Jacques","last_name":"Chirac"}"""
This can be changed for all types at once like just above, or only for specific types, like
implicit def serpentCaseCodecForIdentity: JsonProductCodecFor[Identity] =
JsonProductCodecFor(JsonProductCodec.adapt(_.toSerpentCase))
When encoding / decoding a sealed trait S
, argonaut-shapeless looks
for an implicit JsonSumCodecFor[S]
, which has a field
codec: JsonSumCodec
. A JsonSumCodec
provides a general way of
encoding / decoding sealed traits.
The default JsonSumCodecFor[S]
for all types S
provides
JsonSumCodec.obj
as JsonSumCodec
, which encodes sealed traits
as illustrated above.
JsonSumCodec.typeField
is provided as an alternative,
which discriminates the various cases of a sealed trait by looking
at a field, type
, like
import argonaut._, Argonaut._, ArgonautShapeless._
import argonaut.derive._
implicit def typeFieldJsonSumCodecFor[S]: JsonSumCodecFor[S] =
JsonSumCodecFor(JsonSumCodec.typeField)
sealed trait Base
case class First(i: Int) extends Base
case class Second(s: String) extends Base
val f: Base = First(2)
f.asJson.nospaces
// instead of the default """{"First":{"i":2}}"""
f.asJson.nospaces == """{"type":"First","i":2}"""
Of course, if some of your types already have codecs (defined in their companion object, or manually imported), these will be given the priority over the ones derived by argonaut-shapeless, like
import argonaut._, Argonaut._, ArgonautShapeless._
case class Custom(s: String)
object Custom {
implicit def encode: EncodeJson[Custom] =
EncodeJson.of[String].contramap[Custom](_.s)
implicit def decode: DecodeJson[Custom] =
DecodeJson.of[String].map(Custom(_))
}
Custom("a").asJson.nospaces == """"a""""
""""b"""".decodeOption[Custom] == Some(Custom("b"))
If you want the codec derivation to happen at a controlled spot in your code,
you can use the JsonCodec
annotation. A situation would be that you have
custom codecs and want to make sure they're considered or you don't want to
overgenerate Codecs for ADTs instances.
import argonaut._, Argonaut._
object instances {
import ArgonautShapeless._
import argonaut.derive.JsonCodec
@JsonCodec sealed trait ADT
case class First(i: Int) extends ADT
case class Second(s: String) extends ADT
object ADT // this one's required
}
import instances._
// fails
// val encodeFirst = EncodeJson.of[First]
// works
val encode = EncodeJson.of[Base]
You'll have to add the additional object after all the instances of a sealed trait, see #5.
To use the @JsonCodec
annotation, add MacroParadise to your build.
addCompilerPlugin(
"org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full
)
argonaut-shapeless also has a module to encode/decode types from refined, allowing for some kind of validation at the type level.
Add it to your dependencies with
libraryDependencies += "com.github.alexarchambault" %% "argonaut-refined_6.3" % "1.3.0"
Use like
import argonaut._, Argonaut._, ArgonautShapeless._, ArgonautRefined._
import eu.timepit.refined._
import eu.timepit.refined.api.Refined
case class CC(
i: Int Refined numeric.Greater[W.`5`.T],
s: String Refined string.StartsWith[W.`"A"`.T]
)
CC(
refineMV(6),
refineMV("Abc")
).asJson.nospaces == """{"i":6,"s":"Abc"}""" // fields are encoded as their underlying type
"""{"i": 7, "s": "Abcd"}""".decodeOption[CC] == Some(CC(refineMV(7), refineMV("Abcd")))
"""{"i": 4, "s": "Abcd"}""".decodeOption[CC] == None // fails as the provided `i` doesn't meet the predicate ``GreaterThan[W.`5`.T]``
- spray-json-shapeless features automatic codec derivation with shapeless for spray-json
- circe features its own automatic codec derivation with shapeless, via its generic module
- Alexandre Archambault (@alexarchambault)
- Ben James (@bmjames)
- Denis Mikhaylov (@notxcain)
- Frank S. Thomas (@fthomas)
- Ismael Juma (@ijuma)
- reactormonk (@reactormonk)
- Your name here :-)
Initially based on an early (non Lazy
-based) automatic codec derivation in argonaut
by Maxwell Swadling,
Travis Brown, and
Mark Hibberd.
Released under the BSD license. See LICENSE file for more details.