Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/julienrf/play-json-derived-codecs


https://github.com/julienrf/play-json-derived-codecs

derivation json play-framework

Last synced: 2 days ago
JSON representation

Awesome Lists containing this project

README

        

(Note: this project has been renamed from [play-json-variants](https://github.com/julienrf/play-json-variants/tree/v2.0) to `play-json-derived-codecs`)

# Play JSON Derived Codecs [![](https://index.scala-lang.org/julienrf/play-json-derived-codecs/play-json-derived-codecs/latest.svg)](https://index.scala-lang.org/julienrf/play-json-derived-codecs)

`Reads`, `OWrites` and `OFormat` derivation for algebraic data types (sealed traits and case classes, possibly recursive), powered by [shapeless](http://github.com/milessabin/shapeless).

Compared to the built-in macros, this project brings support for:

- sealed traits ;
- recursive types ;
- polymorphic types.

The artifacts are built for Scala and Scala.js 2.12, and 2.13, Play 3.0 and Shapeless 2.3.

## Versions

For previous versions of Play, you can use previous versions of this library:

| Library version | Play version |
|-----------------------------------------------------------------------------|-----------------|
| [Latest](https://github.com/julienrf/play-json-derived-codecs/releases) | 3.0.x |
| [10.1.0](https://github.com/julienrf/play-json-derived-codecs/tree/v10.1.0) | 2.9.x |
| [7.0.0](https://github.com/julienrf/play-json-derived-codecs/tree/v7.0.0) | 2.8.x |

## Usage

~~~ scala
import julienrf.json.derived

case class User(name: String, age: Int)

object User {
implicit val reads: Reads[User] = derived.reads()
}
~~~

The [API](https://www.javadoc.io/doc/org.julienrf/play-json-derived-codecs_2.12) is simple: the object
`julienrf.json.derived` has just three methods.

- `reads[A]()`, derives a `Reads[A]` ;
- `owrites[A]()`, derives a `OWrites[A]` ;
- `oformat[A]()`, derives a `OFormat[A]`.

### Representation of Sum Types

By default, sum types (types extending a sealed trait) are represented by a JSON object containing
one field whose name is the name of the concrete type and whose value is the JSON object containing
the value of the given type.

For instance, consider the following data type:

~~~ scala
sealed trait Foo
case class Bar(s: String, i: Int) extends Foo
case object Baz extends Foo
~~~

The default JSON representation of `Bar("quux", 42)` is the following JSON object:

~~~ javascript
{
"Bar": {
"s": "quux",
"i": 42
}
}
~~~

### Configuring the Derivation Process

Three aspects of the derivation process can be configured:

- the representation of sum types,
- the way case class field names are mapped to JSON property names,
- the type name used to discriminate sum types.

#### Custom Representation of Sum Types

The default representation of sum types may not fit all use cases. For instance, it is not very
practical for enumerations.

For instance, you might want to represent the `Bar("quux", 42)` value as the following JSON object:

~~~ javascript
{
"type": "Bar",
"s": "quux",
"i": 42
}
~~~

Here, the type information is flattened with the `Bar` members.

You can do so by using the methods in the `derived.flat` object:

~~~ scala
implicit val fooOWrites: OWrites[Foo] =
derived.flat.owrites((__ \ "type").write[String])
~~~

In case you need even more control, you can implement your own `TypeTagOWrites` and `TypeTagReads`.

#### Custom Field Names Mapping

By default, case class fields are mapped to JSON object properties having the same name.

You can transform this mapping by supplying a different `NameAdapter` parameter. For
instance, to use “snake case” in JSON:

~~~ scala
implicit val userFormat: OFormat[User] = derived.oformat(adapter = NameAdapter.snakeCase)
~~~

#### Custom Type Names

By default, case class names are used as discriminators (type tags) for sum types.

You can configure the type tags to use by using the `derived.withTypeTag` object:

~~~ scala
implicit val fooFormat: OFormat[Foo] =
derived.withTypeTag.oformat(TypeTagSetting.FullClassName)
~~~

The library provides the following `TypeTagSetting` values out of the box:

- `TypeTagSetting.ShortClassName`: use the class name (as it is defined in Scala)
- `TypeTagSetting.FullClassName`: use the fully qualified name
- `TypeTagSetting.UserDefinedName`: require the presence of an implicit `CustomTypeTag[A]`
for all type `A` of the sum type, providing the type tag to use

### Custom format for certain types in hierarchy

Sometimes, you might want to represent one type differently than default format would. This can be done by creating an implicit instance of `DerivedReads` or `DerivedWrites` for said type. Below is an example of implementing both custom reads and writes for a single class in a hierarchy:

~~~ scala
sealed trait Hierarchy
case class First(x: Integer)
case class Second(y: Integer)

implicit val SecondReads: DerivedReads[Second] = new DerivedReads[Second] {
def reads(tagReads: TypeTagReads, adapter: NameAdapter) = tagReads.reads("Second", (__ \ "foo").read[Integer].map(foo => Second(foo)))
}

implicit val SecondWrites: DerivedOWrites[Second] = new DerivedOWrites[Second] {
override def owrites(tagOwrites: TypeTagOWrites, adapter: NameAdapter): OWrites[Second] =
tagOwrites.owrites[Second](
"Second",
OWrites[Second](s => JsObject(("foo", Json.toJson(s.y)) :: Nil))
)
}

val defaultTypeFormat = (__ \ "type").format[String]
implicit val HierarchyFormat = derived.flat.oformat[Hierarchy](defaultTypeFormat)
~~~

This will cause `Second` to be read with `SecondReads`, and read with `SecondWrites`.

### Avoiding redundant derivation

By default, the auto-derivation mechanism will be applied to the whole sealed hierarchy. This might be costly in terms of compile-time (as Shapeless is being used under the hood).
To avoid this, it is possible to define an `Format` for the different cases, thus only using auto-derivation for the branching in the sealed trait and nothing else.
~~~ scala
sealed trait Hierarchy

case class First(a: Int, b: Int, c: Int) extends Hierarchy
case class Second(x: Int, y: Int, c: Int) extends Hierarchy

object First {
implicit val format: OFormat[First] = Json.format
}

object Second {
implicit val format: OFormat[Second] = Json.format
}

implicit val HierarchyFormat = derived.oformat[Hierarchy]()
~~~

**Important note**: in case `derived.flat` is being used, it's recommended that the provided `Format`s actually produce `JsObject`s. If that's not the case, a synthetic wrapper around the user-provided result will be generated on-the-fly.
For this reason, `Json.valueFormat` and the like are not compatible with `derived.flat`, and it is best to avoid using them together.

Here is what will happen if they are used together:
~~~ scala
sealed trait Foo
case class Bar(x: Int) extends Foo

object Bar {
implicit val format: Format[Bar] = Json.valueFormat
}

implicit val fooFormat = derived.flat.oformat[Foo]((__ \ "type").format[String])

Json.toJson(Bar(42)) // { "type": "Bar", "__syntheticWrap__": 42 }
~~~

Without the provided `Format`s the derivation mechanism will traverse all the fields in the hierarchy (in this case 6 in total), which may be costly for larger case classes.

Providing the implicits this way can also be used for customization without having to deal with supplying your own type-tags.

## Contributors

See [here](https://github.com/julienrf/play-json-variants/graphs/contributors).

## Changelog

See [here](https://github.com/julienrf/play-json-derived-codecs/releases).