Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot convert from Map[_, Any] #216

Closed
ChetanBhasin opened this issue Mar 2, 2016 · 6 comments
Closed

Cannot convert from Map[_, Any] #216

ChetanBhasin opened this issue Mar 2, 2016 · 6 comments

Comments

@ChetanBhasin
Copy link

While trying out the library, I have realized that there are no implicit conversions for Any or AnyVal.

For example, consider the following code:

case class Container(m: Map[String, AnyVal])
val container = Container(Map("name" -> "John".asInstanceOf[AnyVal], "age" -> 50))
container.asJson

This will not work because there is no implicit mapping for a AnyVal (or for Any, for that matter).

I've been trying to understand how one may write their own implicit conversion guidelines in such cases for converting from objects to JSON and vice versa.

The end goal here is something like being able to convert between any arbitrary JSON to a Map[String, Any], and vice versa.

Note: If such a functionality already exists, please point me to the right direction. I was not able to find this in the documentation or otherwise.

@travisbrown
Copy link
Member

@ChetanBhasin circe is built on the idea of having types drive serialization—you ask for a JSON representation of a value of type Map[String, User], for example, and the appropriate encoders are selected at compile-time—there's no cost or danger of reflection at runtime. circe is entirely helpless when it comes to Any or AnyVal, where you can't do anything reasonable with a value except reflect on it at runtime (if that counts as reasonable).

This is by design, and has two goals: to keep the library simple and safe by avoiding any use of runtime reflection, and to promote the use of types. In many ways Map[String, Any] is the opposite of type-safe functional programming, and circe aims to make it both possible and desirable to avoid having types like Map[String, Any] show up anywhere in your program.

Of course you could write your own Encoder and Decoder instances for types like Map[String, Any], but they'd be unsafe, unidiomatic, and possibly less performant (because of the necessity of runtime reflection), so circe itself will almost definitely never provide anything like that off the shelf.

I'd recommend trying to avoid Map[String, Any] entirely, but if you're stuck with it and you really want to use circe (as opposed to a JSON library that embraces runtime reflection, which is most of them 😄), you could try to cast and convert your Map[String, Any] values into something more type-full, like e.g. Map[String, Either[Int, String]], which circe will happily encode and decode.

@ChetanBhasin
Copy link
Author

@travisbrown Thanks for the reply.

I can totally understand why we might not want to do conversions between Any and JSON value and why custom Encoders are a bad idea.

However, I think, it makes sense to have a data type which deals only with JSON primitives. For example, while the only way to represent a JSON object of key-value pairs in vanilla Scala would be to use a Map[String, Any] or Map[String, AnyVal], we could have something like Map[String, JsType].

JsType in this regard, can be one of the JSON primitives (i.e., String, Long, Boolean, Object, List, or null). However, it would make sense to be able to implicitly convert between JSON primitives to Scala, while backward can be achieved using a JSON type. Of course, in this case, it would be wonderful to be able to do some basic operations on these values (either by implicit conversion or implementing methods).

This approach would be type-safe. Now, my only question is weather something like already exists in the library?
I do realise that this is something fairly simple to implement, but would be great if we could have it included as a feature.

@ChetanBhasin
Copy link
Author

I'm sorry. I've been fiddling around with the library and understand this better. Will close the issue.

@robsonpeixoto
Copy link

@travisbrown I tested Map[String, Either[Int, String]] but it created a json with the Left and Right keys:

import io.circe._, io.circe.generic.auto._, io.circe.parser._, io.circe.syntax._
import io.circe._
import io.circe.generic.auto._
import io.circe.parser._
import io.circe.syntax._
import cats.data.Xor

type Message = Map[String, Either[Int, String]]
object Message {
  def apply(elems: (String, Either[Int, String])*): Map[String, Either[Int, String]] = Map(elems: _*)
}
val m1 = Message("name" -> Right("John"), "age" -> Left(50))
println(m1.asJson)

// Exiting paste mode, now interpreting.

{
  "name" : {
    "Right" : {
      "b" : "John"
    }
  },
  "age" : {
    "Left" : {
      "a" : 50
    }
  }
}

And when I to convert a json without this keys the decode do not work:

// ...
val json: String = """
  {
    "id": 1,
    "name": "Foo",
    "city": "NYC",
    "age": 10,
    "height": 175
  }
"""
val m2 = decode[Message](json)

// Exiting paste mode, now interpreting.
m2: cats.data.Xor[io.circe.Error,Message] = Left(DecodingFailure(CNil, List(El(DownField(id),true,false))))

Is that the expected behave?

@travisbrown
Copy link
Member

@robsonpeixoto The Either constructors are case classes, so generic will produce instances for them just like any other case classes (there are no automatically-provided Either instances, and it's not special-cased).

If you want an untagged representation, you can do something like this:

import io.circe._, io.circe.generic.auto._, io.circe.jawn._, io.circe.syntax._

implicit val encodeIntOrString: Encoder[Either[Int, String]] =
  Encoder.instance(_.fold(_.asJson, _.asJson))

implicit val decodeIntOrString: Decoder[Either[Int, String]] =
  Decoder[Int].map(Left(_)).or(Decoder[String].map(Right(_)))

And then:

scala> println(m1.asJson)
{
  "name" : "John",
  "age" : 50
}

scala> decode[Message](m1.asJson.noSpaces)
res1: cats.data.Xor[io.circe.Error,Message] = Right(Map(name -> Right(John), age -> Left(50)))

Does that work for you?

@robsonpeixoto
Copy link

Very well, @travisbrown. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants