I'm trying to use JSON-inception for vaildating a request inside a RESTful application. But it seems that either the JSON-request or the JSON-inception logic is faulty.
some fragments of the necessary code(simplified):
(JsPath \ "geometry").readNullable[Geometry]
case class Geometry(
circle : Option[Circle]
)
object Geometry{
implicit val circleReads: Format[Circle] = Json.format[Circle]
}
case class Circle(radius: Double)
The JSON-String i am using:
"geometry":{
"circle":{"radius":1.1},
}
Posting this request is resulting in a NullPointerException at this place
request.body.validate[Calculation]
Tests showed that the validation recognizes the "circle"-path in the JSON-string because there is no reaction to other names at this point.
Am i missing something or do i misunderstand the JSON-inception approach? The playframework version is 2.4.6.
Related
I'm very new to Scala and I'm trying to send a json from a client to a service.
I have an interface ("trait") where I have the method receiveData takes a JSONObject as an argument, but apparently it's deprecated.
trait DataService {
def receiveData(data: JSONObject, clientId: String): Unit
}
I have this Scala version ThisBuild / scalaVersion := "3.0.0-RC1"
I have looked around for what I should use instead and found nothing.
it says
#deprecated("Use The Scala Library Index to find alternatives: https://index.scala-lang.org/", "1.0.6")
but I don't know how to search for things there.
Appreciate all help!
EDIT:
ANSWER: there are plenty: Play-Json, zio-json, Circe, Jackson...
Intro
I'm sending JSON messages between two backend servers that use different languages. The producing
server creates a variety of JSON messages, wrapped inside a message with metadata.
The wrapping class is Message, The consuming server has to determine which type of message its
receiving based solely on the message contents.
When I try to use a star-projection to
deserialize the message, I get an error.
Example
import kotlinx.serialization.json.Json
#Language("JSON")
val carJson = """
{
"message_type": "some message",
"data": {
"info_type": "Car",
"name": "Toyota"
}
}
""".trimIndent()
// normally I wouldn't know what the Json message would be - so the type is Message<*>
val actualCarMessage = Json.decodeFromString<Message<*>>(carJson)
Error message
Exception in thread "main" java.lang.IllegalArgumentException: Star projections in type arguments are not allowed, but Message<*>
at kotlinx.serialization.SerializersKt__SerializersKt.serializerByKTypeImpl$SerializersKt__SerializersKt(Serializers.kt:81)
at kotlinx.serialization.SerializersKt__SerializersKt.serializer(Serializers.kt:59)
at kotlinx.serialization.SerializersKt.serializer(Unknown Source)
at ExampleKt.main(example.kt:96)
at ExampleKt.main(example.kt)
Class structure
I want to deserialize JSON into a data class, Message, that has a field with a generic type.
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
#Serializable
data class Message<out DataType : SpecificInformation>(
#SerialName("message_type")
val type: String,
#SerialName("data")
val data: DataType,
)
The field is constrained by a sealed interface, SpecificInformation, with some implementations.
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator
#JsonClassDiscriminator("info_type")
sealed interface SpecificInformation {
#SerialName("info_type")
val infoType: String
}
#Serializable
#SerialName("User")
data class UserInformation(
#SerialName("info_type")
override val infoType: String,
val name: String,
) : SpecificInformation
// there are more implementations...
Workaround?
This is a known
issue (kotlinx.serialization/issues/944)
,
so I'm looking for workarounds.
I have control over the JSON structure and libraries - though I have a preference for
kotlinx.serialization.
I can't change that there are two JSON objects, one is inside the other, and the discriminator is
inside the inner-class.
A custom serializer would be great. But I'd prefer to have this configured on the class or file
(with #Serializable(with = ...) or #file:UseSerializers(...)) as using a
custom SerializersModule is not as seamless.
Attempt: JsonContentPolymorphicSerializer
I've written a custom serializer, which only if it's used specifically (which is something I'd like
to avoid). It's also quite clunky, breaks if the data classes change or a new one is added, and
doesn't benefit from the sealed interface.
Can this be improved so that
It can be used generically? Json.decodeFromString<Message<*>>(carJson)
It doesn't have any hard-coded strings?
class MessageCustomSerializer : JsonContentPolymorphicSerializer<Message<*>>(Message::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Message<*>> {
val discriminator = element
.jsonObject["data"]
?.jsonObject?.get("info_type")
?.jsonPrimitive?.contentOrNull
println("found discriminator $discriminator")
val subclassSerializer = when (discriminator?.lowercase()) {
"user" -> UserInformation.serializer()
"car" -> CarInformation.serializer()
else -> throw IllegalStateException("could not find serializer for $discriminator")
}
println("found subclassSerializer $subclassSerializer")
return Message.serializer(subclassSerializer)
}
}
fun main() {
#Language("JSON")
val carJson = """
{
"message_type": "another message",
"data": {
"info_type": "Car",
"brand": "Toyota"
}
}
""".trimIndent()
val actualCarMessage =
Json.decodeFromString(MessageCustomSerializer(), carJson)
val expectedCarMessage = Message("another message", CarInformation("Car", "Toyota"))
require(actualCarMessage == expectedCarMessage) {
println("car json parsing ❌")
}
println("car json parsing ✅")
}
#Serializable(with = ... - infinite loop
I tried applying MessageCustomSerializer directly to Message...
#Serializable(with = MessageCustomSerializer::class)
data class Message<out T : SpecificInformation>(
//...
But then I couldn't access the plugin-generated serializer, and this causes an infinite loop.
return Message.serializer(subclassSerializer) // calls 'MessageCustomSerializer', causes infinite loop
#Serializer(forClass = ...) - not generic
In addition to annotating Message with #Serializable(with = MessageCustomSerializer::class), I
tried
deriving a plugin-generated serializer:
#Serializer(forClass = Message::class)
object MessagePluginGeneratedSerializer : KSerializer<Message<*>>
But this serializer is not generic, and causes an error
java.lang.AssertionError: No such value argument slot in IrConstructorCallImpl: 0 (total=0).
Symbol: MessageCustomSerializer.<init>|-5645683436151566731[0]
at org.jetbrains.kotlin.ir.expressions.IrMemberAccessExpressionKt.throwNoSuchArgumentSlotException(IrMemberAccessExpression.kt:66)
at org.jetbrains.kotlin.ir.expressions.IrFunctionAccessExpression.putValueArgument(IrFunctionAccessExpression.kt:31)
at org.jetbrains.kotlinx.serialization.compiler.backend.ir.IrBuilderExtension$DefaultImpls.irInvoke(GeneratorHelpers.kt:210)
at org.jetbrains.kotlinx.serialization.compiler.backend.ir.SerializableCompanionIrGenerator.irInvoke(SerializableCompanionIrGenerator.kt:35)
You are asking many things here, so I will simply try to give some pointers in regards to the errors you are making which you seem to be stuck on. With those in mind, and reading the documentation I link to, I believe you should be able to resolve the rest yourself.
Polymorphic serialization
Acquaint yourself with kotlinx.serialization polymorphic serialization. When you are trying to serialize Message<*> and DataType you are trying to use polymorphic serialization.
In case you are serializing Message<*> as the root object, specifying PolymorphicSerializer explicitly (as I also posted in the bug report you link to) should work. E.g., Json.decodeFromString( PolymorphicSerializer( Message::class ), carJson ).
P.s. I'm not 100% certain what you are trying to do here is the same as in the bug report. Either way, specifying the serializer explicitely should work, whether or not it is a bug that you shouldn't be required to do so.
The message_type and info_type fields you have in Message and DataType respectively are class discriminators. You need to configure this in your Json settings, and set the correct SerialName on your concrete classes for them to work. Using a different class discriminator per hierarchy is only possible starting from kotlinx.serialization 1.3.0 using #JsonClassDiscriminator.
Overriding plugin-generated serializer
But then I couldn't access the plugin-generated serializer, and this causes an infinite loop.
#Serializable(with = ...) overrides the plugin-generated serializer. If you want to retain the plugin-generated serializer, do not apply with.
When you are serializing the object directly (as the root object), you can still pass a different serializer to use as the first parameter to encode/decode. When you want to override the serializer to use for a specific property nested somewhere in the root object, use #Serializable on the property.
Polymorphism and generic classes
The "No such value argument slot in IrConstructorCallImpl: 0" error is to be expected.
You need to do more work in case you want to specify a serializer for polymorphic generic classes.
I have some simple messages with implicit Json.reads and Json.formats defined in their companion objects. All of these messages extend MyBaseMessage.
In other words, for any T <: MyBaseMessage, T is (de)serializable.
These messages represent simple CRUD operations to be performed on a cluster, so there's an Play server that will sit between a CLI sending JSON and the Cluster. Because the operations are simple, I should be able to make some very generic Actions on the Play side: when I receive JSON at an endpoint, deserialize the message according to the endpoint and forward that message to the cluster.
My ultimate goal is to do something like this:
// AddBooMessage extends MyBaseMessage
def addBoo = FooAction[AddBooMessage]
// AddMooMessage extends MyBaseMessage
def addMoo = FooAction[AddMooMessage]
// etc. ...
So when a request is sent to the route corresponding to the addBoo message, the request's JSON will be parsed into an AddBooMessage message and pushed to the cluster. Repeat ad nauseam.
I have the following written:
private def FooAction[T <: MyBaseMessage] = Action {
implicit request =>
parseAndForward[T](request)
}
private def parseAndForward[T <: MyBaseMessage](request: Request[AnyContent]) = {
val parsedRequest = Json.parse(request.body.toString).as[T]
Logger.info(s"Got '$parsedRequest' request. Forwarding it to the Cluster.")
sendToCluster(parsedRequest)
}
But I find the following error:
No Json deserializer found for type T. Try to implement an implicit Reads or Format for this type.
However, all of these messages are serializable and have both Reads and Format defined for them.
I tried passing (implicit fjs: Reads[T]) to parseAndForward in hopes to implicitly provide the Reads required (though it should already be implicitly provided), but it didn't help.
How can I solve this problem?
JsValue#as[A] needs an implicit Reads[A] in order to deserialize JSON to some type A. That is, the error message you're getting is caused because the compiler can't guarantee there is a Reads[T] for any type T <: MyBaseMessage. Assuming sendToCluster is parameterized the same way, this can easily by fixed by simply requiring an implicit Reads[T] in each method call. It sounds like you were close, and just needed to take things a step further by requiring the Reads[T] from FooAction, as well (since that call is where the type is determined).
private def FooAction[T <: MyBaseMessage : Reads] = Action { implicit request =>
parseAndForward[T](request)
}
private def parseAndForward[T <: MyBaseMessage : Reads](request: Request[AnyContent]) = {
val parsedRequest = Json.parse(request.body.toString).as[T]
Logger.info(s"Got '$parsedRequest' request. Forwarding it to the Cluster.")
sendToCluster(parsedRequest) // Assuming this returns a `Future[Result]`
}
If your intention if you use the above code by manually supplying the type parameter, this will work just fine.
There are some other improvements I think you can make here. First, if you always expect JSON, you should require the parse.json BodyParser. This will return a BadRequest if what is received isn't even JSON. Second, as will throw an exception if the received JSON cannot be deserialized into the expected type, you can use JsValue#validate to do this more safely and fold the result to handle the success and error cases explicitly. For example:
private def FooAction[T <: MyBaseMessage] = Action.async(parse.json) { implicit request =>
parseAndForward[T](request)
}
private def parseAndForward[T <: MyBaseMessage](request: Request[JsValue]) = {
request.body.validate[T].fold(
error => {
Logger.error(s"Error parsing request: $request")
Future.successful(BadRequest)
},
parsed => {
Logger.info(s"Got '$parsed' request. Forwarding it to the Cluster.")
sendToCluster(parsed)
}
)
}
Anyone can show me how to convert case class class instances to JSON in Play framework (particularly Play v2.3.x) with Scala?
For example I have code like this:
case class Foo(name: String, address: String)
def index = Action {
request => {
val foo = Foo("John Derp", "Jem Street 21") // I want to convert this object to JSON
Ok(Json.toJson(foo)) // I got error at here
}
}
The error message:
Cannot write an instance of com.fasterxml.jackson.data bind.JsonNode
to HTTP response. Try to define a
Writeable[com.fasterxml.jackson.databind.JsonNode]
UPDATE: I found out the above error is caused by wrong import of the Json class, it should be: import play.api.libs.json.Json. However I still got error on implicit problem below.
I have read this tutorial, but when I tried the implicit Writes[Foo] code:
implicit val fooWrites: Writes[Foo] = (
(JsPath \ "name").write[String] and
(JsPath \ "address").write[String]
)(unlift(Foo.unapply))
I got Can't resolve symbol and and Can't resolve symbol unlift error in Intellij. Also the tutorial's code looks complex just for the conversion of an object to JSON. I wonder if there is more simple way to do this?
You can get an Writes[Foo] instance by using Json.writes:
implicit val fooWrites = Json.writes[Foo]
Having this implicit in scope is all you need to convert Foo to JSON. See the documetnation here and here for more info about JSON reads/writes.
The second problem - Can't resolve symbol and - is an Intellij bug introduced in version 1.3 of the Scala plugin. In version 1.3.3 of the Scala plugin, there's now a workaround - set preference checkbox:
Languages & Frameworks > Scala > Core (default) tab > Use old
implicit search algorithm
I have a problem with my custom JSON deserializer.
I use Jackson to map JSON to Java and back. In some cases I need to write my own mapping.
I have an object (filter), which contains a set of another object(metaInfoClass). I try to deserialize the filter with Jackson, but I implemented an own deserializer for the inner object.
The JSON looks like this:
{
"freetext":false,
"cityName":null,
"regionName":null,
"countryName":null,
"maxResults":50,
"minDate":null,
"maxDate":null,
"metaInfoClasses":
[
{
"id":31,
"name":"Energy",
"D_TYPE":"Relevance"
}
],
"sources":[],
"ids":[]
}
My deserializer just works fine, it finds all the fields etc.
The problem is, that somehow (no idea why) the deserializer gets invoked on the rest of the JSON string, so the sources token is getting processed, and so on.
This is very weird, since I don't want to deserialize the big object, but only the inner metaInfoClass.
Even more weird: the CollectionDeserializer class keeps calling my deserializer with the json string even after it is ended. So nothing really happens, but the method gets called.
Any idea?
Thanks a lot!
I was able to find a solution.
I modified the implementation (in the deserialize method) to use to following code:
JsonNode tree = parser.readValueAsTree();
Iterator<Entry<String, JsonNode>> fieldNameIt = tree.getFields();
while (fieldNameIt.hasNext()) {
Entry<String, JsonNode> entry = fieldNameIt.next();
String key = entry.getKey();
String value = entry.getValue().getTextValue();
// ... custom code here
}
So with this approach, it was parsing only the right piece of the code and it's working right now.