I have a need to serialize a handful of case classes to only strings and ints. Meaning, if there's a nested type, it gets serialized as the stringified version of a JSON object, not a JSON object.
Example:
case class Deepest(someNum: Int)
case class Inner(superDeep: Deepest)
case class Outer(aValue: Int, aNestedValue: Inner)
Serializing an instance of Outer Would result in (or something similar)
{
"Outer": {
"aValue": 5,
"aNestedValue": "{ \"superDeep\": .... }"
}
}
Is this possible?
Using jsoniter-scala is the simplest and most efficient way to do it with Scala.
Add dependencies:
libraryDependencies ++= Seq(
// Use the %%% operator instead of %% for Scala.js
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.12.0",
// Use the "provided" scope instead when the "compile-internal" scope is not supported
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.12.0" % "compile-internal"
)
Use a custom codec from this snippet:
import com.github.plokhotnyuk.jsoniter_scala.macros._
import com.github.plokhotnyuk.jsoniter_scala.core._
object Example01 {
sealed trait Message
case class Deepest(someNum: Int)
case class Inner(superDeep: Deepest)
case class Outer(aValue: Int, aNestedValue: Inner) extends Message
implicit val innerCodec: JsonValueCodec[Inner] = new JsonValueCodec[Inner] {
private val codec: JsonValueCodec[Inner] = JsonCodecMaker.make
override def decodeValue(in: JsonReader, default: Inner): Inner =
readFromStringReentrant(in.readString(null))(codec)
override def encodeValue(x: Inner, out: JsonWriter): Unit =
out.writeVal(writeToStringReentrant(x)(codec))
override def nullValue: Inner = null
}
implicit val messageCodec: JsonValueCodec[Message] =
JsonCodecMaker.make(CodecMakerConfig.withDiscriminatorFieldName(None))
def main(args: Array[String]): Unit = {
val message = readFromString[Message](
"""{
| "Outer": {
| "aValue": 5,
| "aNestedValue": "{ \"superDeep\": { \"someNum\": 1 } }"
| }
|}""".stripMargin)
println(writeToString[Message](message, WriterConfig.withIndentionStep(4)))
}
}
Expected output:
{
"Outer": {
"aValue": 5,
"aNestedValue": "{\"superDeep\":{\"someNum\":1}}"
}
}
If more than one type should be stringified you can define a function that takes a regular codec and return a codec that stringify output of original one:
def makeStringifyingCodec[A](codec: JsonValueCodec[A]): JsonValueCodec[A] =
new JsonValueCodec[A] {
override def decodeValue(in: JsonReader, default: A): A =
readFromStringReentrant(in.readString(null))(codec)
override def encodeValue(x: A, out: JsonWriter): Unit =
out.writeVal(writeToStringReentrant(x)(codec))
override def nullValue: A = null.asInstanceOf[A]
}
And use it for field types of the Outer class:
implicit val innerCodec: JsonValueCodec[Inner] =
makeStringifyingCodec(JsonCodecMaker.make[Inner])
implicit val messageCodec: JsonValueCodec[Message] =
JsonCodecMaker.make(CodecMakerConfig.withDiscriminatorFieldName(None))
Related
Consider the following JSON
{
"a": "{\"b\": 12, \"c\": \"test\"}"
}
I would like to define a generic reads Reads[Outer[T]] for this kind of serialized Json
import play.api.libs.json.{Json, Reads, __}
final case class Outer[T](inner: T)
final case class SpecializedInner(b: Int, c: String)
object SpecializedInner {
implicit val reads: Reads[SpecializedInner] = Json.reads[SpecializedInner]
}
object Outer {
implicit def reads[T](implicit readsT: Reads[T]): Reads[Outer[T]] = ???
}
How can I achieve my goal? I tried to flatMap a string-reads for the field "outer.a" but got stuck since I am not able to produce a Reads[T] from the validated JSON
object Outer {
implicit def reads[T](implicit readsT: Reads[T]): Reads[Outer[T]] =
(__ \ "a").read[String].flatMap(x => Json.parse(x).validate[T])
}
You need just add map inside Outer.reads construction after validate[T] invocation.
Please, see next code for example:
object App {
final case class Outer[T](inner: T)
object Outer {
implicit def reads[T](implicit innerReads: Reads[T]): Reads[Outer[T]] = { json: JsValue =>
json.validate[String].flatMap(string => Json.parse(string).validate[T].map(Outer.apply[T]))
}
}
final case class Root[T](a: Outer[T])
object Root {
implicit def reads[T](implicit innerReads: Reads[T]): Reads[Root[T]] = Json.reads
}
final case class SpecializedInner(b: Int, c: String)
object SpecializedInner {
implicit val reads: Reads[SpecializedInner] = Json.reads
}
def main(args: Array[String]): Unit = {
val rawJson = "{\"a\": \"{\\\"b\\\": 12, \\\"c\\\": \\\"test\\\"}\"}"
println(Json.parse(rawJson).validate[Root[SpecializedInner]])
}
}
Which produced next result in my case:
JsSuccess(Root(Outer(SpecializedInner(12,test))),)
Hope this helps!
Experimenting with Circe to serialize and deserialize Algebraic Data Types in Scala I tried out the following sample, following the doc and some samples on the web:
sealed trait StringData
case class Data2(f1: String, f2: String) extends StringData
case class Data3(f1: String, f2: String, f3: String) extends StringData
object StringData {
// ===> does not work, always picks Data2 type
implicit val decodeData: Decoder[Data] = Decoder[OptionsData].map[Data](identity).or(Decoder[TextData].map[Data](identity))
implicit val encodeData: Encoder[StringData] = Encoder.instance {
case d2 # Data2( _,_) => d2.asJson
case d3 # Data3( _, _, _) => d3.asJson
}
def toJson(s: StringData): String = s.asJson.noSpaces
def fromJson(s: String): Either[Error, StringData] = decode[StringData](s)
}
"Inheritance ADT with identical fields" should "serialize and deserialize with Circe" in {
val d2 = Data2("a", "b")
val d3 = Data3("1", "2", "3")
val jd2 = StringData.toJson(d2)
val jd3 = StringData.toJson(d3)
val d2Decoded = StringData.fromJson(jd2)
val d3Decoded = StringData.fromJson(jd3)
d2Decoded.right.get should equal(d2)
d3Decoded.right.get should equal(d3)
println("")
}
The problem is that the type of d3Decoded is always of type Data2 and not the desired Data3.
The solution I came up with was to replace the decoder with this one:
implicit val decodeData: Decoder[StringData] = Decoder.instance { c =>
c.downField("f3").as[String] match {
case m: Either[DecodingFailure, String] if m.isLeft => c.as[Data2]
case m: Either[DecodingFailure, String] if m.isRight => c.as[Data3]
}
}
This seems to me a rather ad-hoc solution. In Jackson there is the possibility to add the type to the Json. I wonder if I use Circe the right way or if this is really the way to do. Any comments very welcome.
Try using discriminator documented here:
import io.circe.generic.extras.auto._
import io.circe.generic.extras.Configuration
import io.circe.parser.decode
implicit val genDevConfig: Configuration =
Configuration.default.withDiscriminator("what_am_i")
sealed trait StringData
case class Data2(f1: String, f2: String) extends StringData
case class Data3(f1: String, f2: String, f3: String) extends StringData
decode[StringData]("""{ "f1": "foo", "f2": "bar", "f3": "qux", "what_am_i": "Data3" }""")
which outputs
res0: Either[io.circe.Error,StringData] = Right(Data3(foo,bar,qux)
where
libraryDependencies ++= Seq(
"io.circe" %% "circe-core",
"io.circe" %% "circe-generic",
"io.circe" %% "circe-parser",
"io.circe" %% "circe-generic-extras",
).map(_ % circeVersion)
I am using the Play Framework and trying to build JSON validator for a class with abstract members. Shown below, the DataSource class is the base class which I am trying to validate the format against.
// SourceTypeConfig Trait.
trait SourceTypeConfig
final case class RDBMSConfig(...) extends SourceTypeConfig
object RDBMSConfig { implicit val fmt = Json.format[RDBMSConfig] }
final case class DirectoryConfig(
path: String,
pathType: String // Local, gcloud, azure, aws, etc.
) extends SourceTypeConfig
object DirectoryConfig { implicit val fmt = Json.format[DirectoryConfig] }
// FormatConfig trait.
trait FormatConfig
final case class SQLConfig(...) extends FormatConfig
object SQLConfig { implicit val fmt = Json.format[SQLConfig]}
final case class CSVConfig(
header: String,
inferSchema: String,
delimiter: String
) extends FormatConfig
object CSVConfig { implicit val fmt = Json.format[CSVConfig]}
// DataSource base class.
case class DataSource(
name: String,
sourceType: String,
sourceTypeConfig: SourceTypeConfig,
format: String,
formatConfig: FormatConfig
)
What I am hoping to accomplish:
val input: JsValue = Json.parse(
"""
{
"name" : "test1",
"sourceType" : "directory",
"sourceTypeConfig" : {"path" : "gs://test/path", "pathType" "google"},
"format" : "csv",
"formatConfig" : {"header" : "yes", "inferSchema" : "yes", "delimiter" : "|"}
}
"""
)
val inputResult = input.validate[DataSource]
What I am struggling with is building the DataSource object and defining its reads/writes/format. I would like it to contain a match based on the sourceType and format values that direct it to point towards the associated sourceTypeConfig and formatConfig's formats so it can parse out the JSON.
Instead of building a parser at the DataSource level, I defined parsers at the SourceConfig and FormatConfig levels, similar to what is shown below.
sealed trait SourceConfig{val sourceType: String}
object SourceConfig{
implicit val fmt = new Format[SourceConfig] {
def reads(json: JsValue): JsResult[SourceConfig] = {
def from(sourceType: String, data: JsObject): JsResult[SourceConfig] = sourceType match {
case "RDBMS" => Json.fromJson[RDBMSConfig](data)(RDBMSConfig.fmt)
case "directory" => Json.fromJson[DirectoryConfig](data)(DirectoryConfig.fmt)
case _ => JsError(s"Unknown source type: '$sourceType'")
}
for {
sourceType <- (json \ "sourceType").validate[String]
data <- json.validate[JsObject]
result <- from(sourceType, data)
} yield result
}
def writes(source: SourceConfig): JsValue =
source match {
case b: RDBMSConfig => Json.toJson(b)(RDBMSConfig.fmt)
case b: DirectoryConfig => Json.toJson(b)(DirectoryConfig.fmt)
}
}
}
Then, DataSource could be simply defined as:
object DataSource { implicit val fmt = Json.format[DataSource] }
Another option is to use play-json-derived-codecs library:
libraryDependencies += "org.julienrf" %% "play-json-derived-codecs" % "4.0.0"
import julienrf.json.derived.flat
implicit val format1: OFormat[RDBMSConfig] = Json.format[RDBMSConfig]
implicit val format2: OFormat[DirectoryConfig] = Json.format[DirectoryConfig]
implicit val format3: OFormat[SourceTypeConfig] = flat.oformat((__ \ "sourceType").format[String])
I use Scala case classes and Play "format" to validate JSON messages. For example:
case class Person(name: String)
implicit val formatPerson = Json.format[Person]
The following JSON:
{
"name": "Alice"
}
would be validated through the method
Json.validate[Person](json)
Now I would like to validate a JSON message with a field "x" that could be a String or a Integer.
For example the two following messages would be both validated with the same method:
{
"x": "hello"
}
{
"x": 8
}
I tried with the following trick but it does not work:
case class Foo(x: Either[String,Int])
implicit val formatFoo = Json.format[Foo]
When I try to define the format for Foo class, the compiler says: "No apply function found matching unapply parameters". Thanks in advance.
You'll have to define a custom Format[Foo]. This should work for Play 2.4
implicit val formatFoo = new Format[Foo]{
override def writes(o: Foo): JsValue = o.x match {
case Left(str) => Json.obj("x" -> str)
case Right(n) => Json.obj("x" -> n)
}
override def reads(json: JsValue): JsResult[Foo] = json \ "x" match {
case JsDefined(JsString(str)) => JsSuccess(Foo(Left(str)))
case JsDefined(JsNumber(n)) if n.isValidInt => JsSuccess(Foo(Right(n.toInt)))
case _ => JsError("error")
}
}
I'm having some trouble figuring out why compiler complains about not finding an implicit parameter for reads because I'm almost sure that it is in the scope. The error is the following:
Error:(13, 18) No Json deserializer found for type Config. Try to implement an implicit Reads or Format for this type.
test.validate[Config].map {
^
Error:(13, 18) not enough arguments for method validate: (implicit rds: play.api.libs.json.Reads[Config])play.api.libs.json.JsResult[wings.m2m.conf.model.Config].
Unspecified value parameter rds.
test.validate[Config].map {
^
and it happens in the following code:
import play.api.libs.json._
import play.api.libs.json.Reads._
import Config.JsonImplicits._
import scala.util.Try
object Test {
def main(args: Array[String]) {
val test = Json.obj("action" -> Config.Action.nameAcquisitionRequest.toString, "value" -> "hola")
test.validate[Config].map {
t => println(t)
t
}
}
}
/**
* Config companion object
*/
object Config {
type ValueType = String
val ActionKey = "action"
val ValueKey = "value"
object Action extends Enumeration {
type Action = Value
val nameAcquisitionRequest = Value("nameAcquisitionRequest")
val nameAcquisitionReject = Value("nameAcquisitionReject")
val nameAcquisitionAck = Value("nameAcquisitionAck")
val broadcast = Value("broadcast")
}
/**
* Json implicit conversions
*/
object JsonImplicits {
implicit object ConfigReads extends Reads[Config] {
def hTypeCast(action: Config.Action.Value, value: Config.ValueType): Config = {
action match {
case Config.Action.nameAcquisitionRequest => NameAcquisitionRequest(value)
case Config.Action.nameAcquisitionReject => NameAcquisitionReject(value)
case Config.Action.nameAcquisitionAck => NameAcquisitionAck(value)
}
}
override def reads(json: JsValue): JsResult[Config] = json match {
case json: JsObject =>
val action = (json \ ActionKey).as[String]
Try(Config.Action.withName(action)) map {
a =>
val value = (json \ ValueKey).as[String]
JsSuccess(hTypeCast(a, value))
} getOrElse (JsError("Can't convert to Config"))
case _ => JsError("Can't convert to Config")
}
}
implicit object ConfigWrites extends OWrites[Config] {
def jsObjectCreator(action: Config.Action.Value, value: Config.ValueType): JsObject = {
Json.obj(ActionKey -> action.toString, ValueKey -> Json.toJson(value))
}
override def writes(o: Config): JsObject = o match {
case c: NameAcquisitionRequest => jsObjectCreator(Config.Action.nameAcquisitionRequest, c.value)
case c: NameAcquisitionReject => jsObjectCreator(Config.Action.nameAcquisitionReject, c.value)
case c: NameAcquisitionAck => jsObjectCreator(Config.Action.nameAcquisitionAck, c.value)
}
}
}
}
sealed trait Config {
val value: Config.ValueType
}
/**
* Intermediate config message
* #param value
*/
case class NameAcquisitionRequest(override val value: String)
extends Config
case class NameAcquisitionReject(override val value: String)
extends Config
case class NameAcquisitionAck(override val value: String)
extends Config
case class Broadcast(override val value: String)
extends Config
the error occurs when executing the main method on the Test object. To make this example work, make sure to add the following dependency in the SBT: "com.typesafe.play" %% "play-json" % "2.4.1" . And I'm not sure, but maybe this resolver is needed: resolvers += "Typesafe Repo" at "http://repo.typesafe.com/typesafe/releases/"
I am not sure what you are trying to achieve and whether this solves your problem but here you go:
test.validate[Config](Config.JsonImplicits.ConfigReads).map {
t => println(t)
t
}