I'm calling different APIs, that use the same key name in the JSON file. Depending on the response, there's one field that may be different types.
To be clear:
The key "results" when calling the API nº1 is a JSON object
The key "results" when calling the API nº2 is a JSON array
My code looks like this when using the second API:
data class Result(
#SerializedName("results") var persons:ArrayList<Person> =ArrayList()
)
The question is if there's any way to use the same class, without taking care if it's a JSON array or a JSON object.
I believe you can define results as an instance of com.fasterxml.jackson.databind.JsonNode.
data class Result(
val results: JsonNode
)
Then you can process results based on it's type—whether it is an ArrayNode or an ObjectNode (as both extend JsonNode):
fun processResults(results: JsonNode) = when{
results.isArray -> processArrayNode(results)
else -> processObjectNode(results)
}
private fun processArrayNode(list: JsonNode): *return whatever you need*{
val elements = list
.elements()
.asSequence()
.toList()
val mappedElements = elements.map{
processObjectNode(it)
}
// do whatever you need with the array
}
private fun processObjectNode(person: JsonNode): *return whatever you need*{
//** this will transform the json node into a linkedHashMap where the keys are the json keys and the values are the values (here interpreted as jsonNodes) **/
val fieldsMap = person
.fields()
.asSequence()
.associateBy( {it.key}, {it.value} )
// process whatever you need
}
This is one way to use the same DTO for both API calls. In my opinion, it is not worth the extra work. I would create two DTOs containing the results field, where in one it is an instance of Person, and in the other it is an instance of List<Person>.
Edit: One little upgrade to the above snippet would be to add extension methods to JsonNode:
fun JsonNode.elementsToList(): List<JsonNode> = this
.elements()
.asSequence()
.toList()
fun JsonNode.fieldsToMap(): Map<String, JsonNode> = this
.fields()
.asSequence()
.associateBy({it.key}, {it.value})
You can use ObjectMapper.typeFactory.constructParametricType to handle generic types:
data class Result<T>(
var x:T
)
val om = ObjectMapper()
om.registerModule(KotlinModule())
val parsedList = om.readValue<Result<List<String>>>(
"""{"x":["x1", "x2"]}""",
om.typeFactory.constructParametricType(Result::class.java, List::class.java)
)
println(parsedList)
val parsedMap = om.readValue<Result<Map<String, String>>>(
"""{"x":{"k1": "v1", "k2": "v2"}}""",
om.typeFactory.constructParametricType(Result::class.java, Map::class.java)
)
println(parsedMap)
Gives output:
Result(x=[x1, x2])
Result(x={k1=v1, k2=v2})
Related
I'm trying to implement a general serialization framework to convert outgoing and incoming messages to json using the kotlinx serialialization. I'm developing a multiplatform app, so I'm trying to get it to run on KotlinJVM and KotlinJS.
For this, I add a type field to every message and use a map that maps each type string to a KClass. What's the type for that map? It contains KClass<> objects whose classes extend the Message class, therefore in java I'd specify my map as
Map<KClass<? extends Message>, String>.
How can I do that in Kotlin?
Afterwards I need to serialize and deserialize the message based on its key and therefore type. Java frameworks take a Class parameter for the type of the object I want to deserialize/instantiate (e.g. gson.fromJson(ClientMessage.class)). In Kotlin this is done using reified parameters Json.decodeFromString<Type>. I do not know the type of the message at compile time though and just have a reference to a KClass, how can I instantiate an object based on that?
#Serializable
open class Message(val type: String) {
companion object {
val messageTypes: Map<KClass<out Message>, String> = mapOf(
ClientLoginMessage::class to "clientLoginMessage",
Message::class to "message"
)
inline fun <reified T> getMessageTypeByClass(): String = messageTypes[T::class]!! // utility for defining the type in the constructors of the individual messages
}
fun toJson() = Json.encodeToString(this)
fun fromJson(json: String): Message? {
val plainMessage = Json.decodeFromString<Message>(json) // get type string from json
return messageTypes.entries.find { it.value == plainMessage.type }?.let {
// how can I use the KClass from it.key as reified parameter?
Json.decodeFromString<?????>(json)
}
}
}
#Serializable
class ClientLoginMessage
: Message(Message.getMessageTypeByClass<ClientLoginMessage>()) {}
Create a map of serializers like for types:
val serializers: Map<KClass<out Message>, KSerializer<out Message>> = mapOf(
ClientLoginMessage::class to ClientLoginMessage.serializer(),
Message::class to Message.serializer()
)
Pass in the serializer needed to Json.decodeFromString like this:
fun fromJson(json: String): Message? {
val plainMessage = Json.decodeFromString<Message>(json) // get type string from json
return messageTypes.entries.find { it.value == plainMessage.type }?.let {
// how can I use the KClass from it.key as reified parameter?
Json.decodeFromString(serializers.get(plainMessage.type)!!, json)
}
}
You might also want to have a look at the Kotlin built in handling of polymorphic classes: https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md
I am trying to parse a JSON response I am receiving from an 3rd party API.
The response returns a json object with N child objects.
The children all conform to the same model/class, but the amount of children can change.
Had it been an array of objects, it would have been trivial, but I am not sure how to do it with an object holding N objects.
I believe I need a custom typeadapter but I can't seem to get it done.
Here is the JSON:
"api":{
"results": 94
"leagues": {
"1":{
"league_id":"1"
"name":"2018 Russia World Cup"
"country":"World"
"season":"2018"
"season_start":"2018-06-14"
}
"2":{...}
"3":{...}
"4":{...}
...
"N":{...}
}
}
So basically it is the "leagues" object I am trying to parse.
I am hoping to end up with a List<League>
For instance, the the root object could have this model:
class Api {
val results: Int
val leagues: List<League>
}
Personally I'd go for a Map<String, League> (assuming the entries in the map would be of class League) for the type of leagues.
class Api {
val results: Int
val leagues: Map<String, League>
}
I think the things to consider here are mostly regarding the order I suppose. If you need to maintain the order of the entries, I'm not sure if Moshi does it automatically or if you need to use a specific implementation of Map to guarantee this.
You can get make the league list in a custom adapter.
data class Api(val results: Int, val leagues: List<League>)
object LeagueListAdapter {
#FromJson fun fromJson(reader: JsonReader, leagueAdapter: JsonAdapter<League>): List<League> {
reader.beginObject()
val result = mutableListOf<League>()
while (reader.hasNext()) {
reader.skipName()
result += leagueAdapter.fromJson(reader)!!
}
reader.endObject()
return result
}
}
Don't forget to add the adapter when building your Moshi instance (Moshi.Builder.add).
I'm new to scala and play framework.
Why does scala not have something like this??
class Customer (idx: Int, emailx: String) {
val id: Int = idx
val email: String = emailx
}
....
def customers = Action {
val customer = new Customer(1, "Customer1")
Ok(Json.toJson(customer))
}
I like play frameowrk (with scala, its productivity)
But,
Why should I map each field of my object manulay to json field?? Was it so hard for scala to implement this future like in Java or C#, even php has json_encode.
Is there any way to achieve this simple goal (return object as json) without any additional manipulations?
Macros are slick and perfect for generating simple case class formats
implicit val jsonFormat = Json.format[Customer]
Typically you put this declaration in your companion object to the type you are generating a format for. This way it is implicitly in scope in any file that you import your type (Customer). Like this:
case class Customer(...)
object Customer {
implicit val jsonFormat = Json.format[Customer]
}
Then in your controller you can do
Json.toJson(customer)
which will produce the JsValue type expected by Play.
For my classes I define a convertor, so that I can write exactly what you have written, e.g. Json.toJson(customer), but the convertor, though simple, does currently have to be written once. E.g.
implicit val customerWrites = new Writes[Customer] {
def writes(customer:Customer) = Json.obj(
"id" -> customer.id,
"email" -> customer.email
)
}
Perhaps macros, into which I have not delved, could do this more automatically...
I just saw that there are some libraries for running a Dart web server, like Start.
So I was thinking something like this..
If both client and server code is written in Dart, is it possible to send "Dart objects" via websockets (or normal REST for that matter) so that the type information remains on the other end? Or do I need to serialize/deserialize via JSON or something similar on the way? Or am I over thinking things here?
regards Oskar
You will need to serialize the Dart object somehow. You can try JSON, or you can try the heavy-duty serialization package.
There is no fully automatic JSON serialization for custom Dart classes. You will need to add a custom toJson serializer and create some sort of fromJson constructor.
e.g. if you had a Person class, you could do something like this:
import 'dart:json' as json;
class Person {
String name;
int age;
Person(this.name, this.age);
Person.fromJson(String json) {
Map data = json.parse(json);
name = data['name'];
age = data['age'];
}
Map toJson() {
return {'name': name, 'age': age};
}
}
Note: the fromJson is just a convention. You will need to call it somehow, there is no built-in mechanism to take an arbitrary JSON string and call the right constructors on your custom object.
As mentioned above, the serialization package is more heavy weight, but much more full featured. Here is an example from its docs:
// uses the serialization package
var address = new Address();
address.street = 'N 34th';
address.city = 'Seattle';
var serialization = new Serialization()
..addRuleFor(address);
Map output = serialization.write(address);
and
// uses serialization
var serialization = new Serialization()
..addRuleFor(address,
constructor: "create",
constructorFields: ["number", "street"],
fields: ["city"]);
You can use the 'exportable' package to render your class to JSON or a map in a more declarative fashion.
import 'package:exportable/exportable.dart';
class Product extends Object with Exportable
{
#export String ProductName;
#export num UnitPrice;
#export bool Discontinued;
#export num UnitsInStock;
Product(this.ProductName, this.UnitPrice, this.Discontinued, this.UnitsInStock);
}
Product prod = new Product("First", 1.0, false, 3 );
var json = prod.toJson(); // {"ProductName":"First","UnitPrice":1.0,"Discontinued":false,"UnitsInStock":3}
I need to serialize/deserialize a Scala class with structure something like the following:
#JsonIgnoreProperties(ignoreUnknown = true, value = Array("body"))
case class Example(body: Array[Byte]) {
lazy val isNativeText = bodyIsNativeText
lazy val textEncodedBody = (if (isNativeText) new String(body, "UTF-8") else Base64.encode(body))
def this(isNativeText: Boolean, textEncodedBody: String) = this((if(isNativeText) str.getBytes("UTF-8") else Base64.decode(textEncodedBody)))
def bodyIsNativeText: Boolean = // determine if the body was natively a string or not
}
It's main member is an array of bytes, which MIGHT represent a UTF-8 encoded textual string, but might not. The primary constructor accepts an array of bytes, but there is an alternate constructor which accepts a string with a flag indicating whether this string is base64 encoded binary data, or the actual native text we want to store.
For serializing to a JSON object, I want to store the body as a native string rather than a base64-encoded string if it is native text. That's why I use #JsonIgnoreProperties to not include the body property, and instead have a textEncodedBody that gets echoed out in the JSON.
The problem comes when I try to deserialize it like so:
val e = Json.parse[Example]("""{'isNativeText': true, 'textEncodedBody': 'hello'}""")
I receive the following error:
com.codahale.jerkson.ParsingException: Invalid JSON. Needed [body],
but found [isNativeText, textEncodedBody].
Clearly, I have a constructor that will work...it just is not the default one. How can I force Jerkson to use this non-default constructor?
EDIT: I've attempted to use both the #JsonProperty and #JsonCreator annotation, but jerkson appears to disregard both of those.
EDIT2: Looking over the jerkson case class serialization source code, it looks like a case class method with the same name as its field will be used in the way that a #JsonProperty would function - that is, as a JSON getter. If I could do that, it would solve my problem. Not being super familiar with Scala, I have no idea how to do that; is it possible for a case class to have a user-defined method with the same name as one of its fields?
For reference, here is the code below that leads me to this conclusion...
private val methods = klass.getDeclaredMethods
.filter { _.getParameterTypes.isEmpty }
.map { m => m.getName -> m }.toMap
def serialize(value: A, json: JsonGenerator, provider: SerializerProvider) {
json.writeStartObject()
for (field <- nonIgnoredFields) {
val methodOpt = methods.get(field.getName)
val fieldValue: Object = methodOpt.map { _.invoke(value) }.getOrElse(field.get(value))
if (fieldValue != None) {
val fieldName = methodOpt.map { _.getName }.getOrElse(field.getName)
provider.defaultSerializeField(if (isSnakeCase) snakeCase(fieldName) else fieldName, fieldValue, json)
}
}
json.writeEndObject()
}
Correct me if I'm wrong, but it looks like Jackson/Jerkson will not support arbitrarily nested JSON. There's an example on the wiki that uses nesting, but it looks like the target class must have nested classes corresponding to the nested JSON.
Anyway, if you're not using nesting with your case classes then simply declaring a second case class and a couple implicit conversions should work just fine:
case class Example(body: Array[Byte]) {
// Note that you can just inline the body of bodyIsNativeText here
lazy val isNativeText: Boolean = // determine if the body was natively a string or not
}
case class ExampleRaw(isNativeText: Boolean, textEncodedBody: String)
implicit def exampleToExampleRaw(ex: Example) = ExampleRaw(
ex.isNativeText,
if (ex.isNativeText) new String(ex.body, "UTF-8")
else Base64.encode(ex.body)
)
implicit def exampleRawToExample(raw: ExampleRaw) = Example(
if (raw.isNativeText) raw.textEncodedBody.getBytes("UTF-8")
else Base64.decode(textEncodedBody)
)
Now you should be able to do this:
val e: Example = Json.parse[ExampleRaw](
"""{'isNativeText': true, 'textEncodedBody': 'hello'}"""
)
You could leave the original methods and annotations you added to make the JSON generation continue to work with the Example type, or you could just convert it with a cast:
generate(Example(data): ExampleRaw)
Update:
To help catch errors you might want to do something like this too:
case class Example(body: Array[Byte]) {
// Note that you can just inline the body of bodyIsNativeText here
lazy val isNativeText: Boolean = // determine if the body was natively a string or not
lazy val doNotSerialize: String = throw new Exception("Need to convert Example to ExampleRaw before serializing!")
}
That should cause an exception to be thrown if you accidentally pass an instance of Example instead of ExampleRaw to a generate call.