scalalandio / chimney

Scala library for boilerplate-free, type-safe data transformations
https://chimney.readthedocs.io
Apache License 2.0
1.17k stars 94 forks source link

Feature request: Support pluggable, map-like models #95

Closed jhalterman closed 5 years ago

jhalterman commented 5 years ago

My use case is object mapping to and from case classes to Map-like structures such as Jackson's ObjectNode. We do a lot of this when serializing data to and from JSON. It would be awesome if Chimney supported mapping to Map-like models where fields on the Map side are referenced by their name as strings. Ex:

val personNode: ObjectNode = person.into[ObjectNode]
  .with(_.name, "theName")
  .with(_.age, "theAge")

And the reverse:


val person: Person = personNode.into[Person]
  .with("theName", _.name)
  .with("theAge", _.age)

Obviously to make this work with external models that Chimney doesn't know about, it would need a simple SPI that could allow external model support to be plugged into Chimney. Ex:

trait MapLikeSupport[T] {
  def create(): T

  def set(map: T, key: String, value: Any): T

  def get(map: T, key: String)
}

Making it better

Since the main use case for this is serializing/deserializing from JSON to case classes, it would be awesome if Chimney supported bi-directional mappings for the above, so a mapping could be defined once and used in each direction. Ex:

case class Person(name: String, age: String)
val mapper = mapperFor[Person, ObjectNode]
  .with(_.name, "theName")
  .with(_.age, "theAge")

val personNode = mapper.mapTo(person1) // serialize
val person2 = mapper.mapFrom(personNode) // deserialize
MateuszKubuszok commented 5 years ago

I am not sure what is the actual use case here.

If it's only about serialization, then there is a plenty libraries that do exactly that e.g. Circe. Because functions are composable, you could use Circe generic/derivation to create Encoders/Decoders. If there is a mismatch between JSON and case class format then optics can solve that.

If it's not about serialization. but some generic structure I am still not sure what would be benefit of using chimney here. Configuration of a custom behavior could require so much code, that writing shapeless by hand could be faster (or maybe magnolia). The main driving force behind chimney is that both input and output format are known at compile time and the transformation cannot fail.

jhalterman commented 5 years ago

The use case is indeed serialization. What's different about Chimney vs Circe (AFAICT) is that to my eyes, Chimney looks like an object mapper whereas Circe looks like a serializer.

With Chimney you're describing how the fields between two types match each other (ex: .withFieldRenamed(_.addict, _.forAddict)) using actual field references (which are refactoring safe) without having to explicitly construct the destination object and pass in the fields, and that's awesome. This also means that in theory you could use the same mappings bi-directionally, as some object mappers do.

Of course Chimney mappings currently appear to be defined per object instance (ex: command.into[CoffeeMade]) rather than per type (ex: Chimney.mapperFor[Command, CoffeeMade], but I assume per type should be possible. This alone would be a nice enhancement to Chimney, where the same mappings could be used on multiple instances of some type.

You're right though that as far as mapping to map-like structures goes, you can't guarantee that the types will match for each field in the map, but that's a general deserialization problem.

krzemin commented 5 years ago

Regarding mapping between types, you can always define implicit Transformer[A, B] which you can tune as you want. Then such transformer is picked from implicit scope by the macro each time we require transformation from A to B (including nested types). I can't see urgent reason to provide extra syntax for it.

What you originally suggested was not exactly mapping (transforming) task, but (de)serialization from map-like structures which are known not to be strongly typed, thus you have to deal with error handling (missing fields, wrong types, etc.). This is not in scope of Chimney library.

Instead, for such use case I would suggest either to:

jhalterman commented 5 years ago

A custom Transformer doesn't save me from having to write a bunch of boilerplate for all my serializations, twice (one for each direction). I suspect that Circe and others can't be adapted to what I'm trying to achieve since they don't capture field references like Chimney does, but this idea not being within the scope of Chimney is fair enough. Thanks for the response.