I have a JSON string in which the keys are not predictable.
The server returns the values with different keys with each response.
Sample JSON looks like -
{
"ids": {
"123": "08:10",
"456": "08:00"
}
}
Here, keys 123 and 345 are not fixed i.e. on the next request, my response would look as below -
{
"ids": {
"123": "08:10",
"456": "08:00"
}
}
Now, I want to parse this response into an object using GSON. So, I created the model classes as below -
data class SlotsResponse(
val ids: IDs
)
data class IDs(
val id: Map<String, String>
)
And in the code, I am trying to deserialize it as -
val response = Gson().fromJson(strResponse, SlotsResponse::class.java)
But, I am unable to get the values for IDs. They are null.
Can someone please help me to understand whatever I am trying to achieve is possible?
What you have represented with your current model contains one extra nested object. So it would represent JSONs that look like this:
{
"ids": {
"id": {
"123": "08:10",
"456": "08:00"
}
}
}
In your actual JSON, there is no field named id, so you only need the root object with the field ids, and the dynamic map:
data class SlotsResponse(
val ids: Map<String, String>
)
Related
I have a linkedhashmap that has the following shape: <String, Subject>. The class Subject has the following fields:
class Subject {
var name: Boolean? = null
var lastname: Boolean? = null
var location: Boolean? = null
..
}
final_result =
"admin" -> Subject
"customer" -> Subject
etc.
I need to populate data classes that have the following format:
data class SubjectSummary(
val admin: SubjectData,
val customer: SubjectData
...
)
data class SubjectData(val details: DetailsData)
data class DetailsData(val name:String, val lastName:String ...)
Because I need to serialize the SubjectSummary class and get the following json format:
{
"admin": {
"details": {
"name": "",
"lastname": "",
...
}
}
"customer": {
"details": {
"name": "",
"lastname": "",
...
}
}
}
How do I assign the final_result map to match the SubjectSummary structure? I have done it with simple data classes, but when the fields within the data class are data classes, I'm not sure hot to populate it. Any guidance?
For simplicity I'm only showing a small example with a few fields.
If your goal with this transformation is just to be able to serialize with the given JSON format, then you don't need this SubjectSummary class. A Map<String, SubjectData> would be sufficient and probably more convenient to create when transforming from the initial map.
Also, it seems that DetailsData contains the same fields as Subject. If that's the case there is no need for an extra class either.
So in the end it seems you just need to create a Map<String, SubjectData where SubjectData could be defined as data class SubjectData(val details: Subject). You can transform your initial map pretty easily then:
val transformed = finalResult.mapValues { (_, subject) -> SubjectData(subject) }
This is a simplified example of the JSON I want to work with:
{
"useless_info": "useless info",
"data": {
"useless_info2": "useless info 2",
"children": [
{
"kind": "Car",
"data": {
"id": 1,
"transmission": "manual"
}
},
{
"kind": "Boat",
"data": {
"id": 2,
"isDocked": true
}
}
]
}
}
children is an array of vehicle objects. vehicle can be boat or car.
My Problem
The information that I want is nested quite deep (the real JSON is much deeply nested). A hack solution is to model the JSON exactly by writing dozens of nested data classes that references each other. I do not want to do this.
My problem is that while I know how to use JsonTransformingSerializer to unwrap arrays of a single type, and JsonContentPolymorphicSerializer to work with objects of various types, in this situation I believe I require both, but I can't get it to work.
What I Did
Assuming a single type
I tried understanding how it would work if it was a single type.
If the objects I wanted where all of the same type, it would be trivial to implement a JsonTransformingSerializer to cut right into the data I want. In this example, I will assume I only care about the ID, so I can just create a generic Vehicle model.
#Serializable
data class VehicleResponse(
#Serializable(with = VehicleResponseSerializer::class)
#SerialName("data")
val vehicles: List<Vehicle>
)
#Serializable
data class Vehicle(val id: Int)
object VehicleResponseSerializer : JsonTransformingSerializer<List<Vehicle>>(ListSerializer(Vehicle.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement {
val vehicles = mutableListOf<JsonElement>()
// equals: [{"kind":"Car","data":{"id":1,"transmission":"manual"}},{"kind":"Boat","data":{"id":2,"isDocked":true}}]
val vehicleArray = element.jsonObject["children"]!!.jsonArray
vehicleArray.forEach { vehicle ->
// equals: {"id":1,"transmission":"manual"}}
val vehicleData = vehicle.jsonObject["data"]!!
vehicles.add(vehicleData)
}
return JsonArray(vehicles.toList())
}
}
The code works perfectly. Calling it out from main, printing the result gives me:
VehicleResponse(vehicles=[Vehicle(id=1), Vehicle(id=2)])
But they are actually Polymorphic!
Assuming one type does not work. I need to work with Car and Boat, and call their respective functions and properties.
I tried to model the structure like this:
#Serializable
data class VehicleResponse(
#Serializable(with = VehicleResponseSerializer::class)
#SerialName("data")
val vehicles: List<Vehicle>
)
#Serializable
abstract class Vehicle {
abstract val id: Int
}
#Serializable
data class Car(
override val id: Int,
val transmission: String,
) : Vehicle()
#Serializable
data class Boat(
override val id: Int,
val isDocked: Boolean,
) : Vehicle()
What I Want
I want to receive a JSON from a server, and instantly be able to deserialize it into a list of Vehicle objects, like the one VehicleResponse has.
I want to navigate through a deeply nested JSON, and unwrap an array that contains various Vehicle objects. For this, I assume I need to use JsonTransformingSerializer.
I want to use polymorphic deserialization to convert each of Vehicle into its corresponding subtype.
The actual ACTUAL problem
The thing that is truly throwing me in a loop is that a polymorphic serializer just does not seem to fit. It's called first before I get to parse the JSON. How am I supposed to decide which serializer to use?
Here's a test implementation:
object VehiclePolymorphicSerializer: JsonContentPolymorphicSerializer<VehicleResponse>(VehicleResponse::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out VehicleResponse> {
println("\nselectDeserializer()\n" +
"base element:\n" +
"$element\n")
// this return is a temporary hack, I just want to see the base element by printing it to the console
return VehicleResponse.serializer()
}
}
It prints:
selectDeserializer()
base element:
{"useless_info":"useless info","data":{"useless_info2":"useless info 2","children":[{"kind":"Car","data":{"id":1,"transmission":"manual"}},{"kind":"Boat","data":{"id":2,"isDocked":true}}]}}
That's the whole initial JSON! How am I supposed to decide which deserialization strategy to use, if both Car and Boat are in there? The JsonTransformingSerializer is called after the JsonContentPolymorphicSerializer.
Really not sure how am I supposed to proceed here. Would really appreciate even a slight hint.
kotlinx.serialization can handle polymorphic deserialization in this case without custom JsonContentPolymorphicSerializer. You just need to preserve class descriminator in the JSON returned from your JsonTransformingSerializer:
object VehicleResponseSerializer : JsonTransformingSerializer<List<Vehicle>>(ListSerializer(Vehicle.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement {
// equals: [{"kind":"Car","data":{"id":1,"totalWheels":"4"}},{"kind":"Boat","data":{"id":2,"isDocked":true}}]
val vehicleArray = element.jsonObject["children"]!!.jsonArray
// equals: [{"type":"Car","id":1,"totalWheels":"4"}, {"type":"Boat","id":2,"isDocked":true}]
return JsonArray(vehicleArray.map {
val data = it.jsonObject["data"]!!.jsonObject
val type = it.jsonObject["kind"]!!
JsonObject(
data.toMutableMap().apply { this["type"] = type }
/*
//Kotlin 1.4 offers a nicer way to do this:
buildMap {
putAll(data)
put("type", type)
}
*/
)
})
}
}
If you declare Vehicle class as sealed (not just abstract), you're already good to go. If you want to keep it abstract, then you need to register all its subclasses in serializersModule
val module = SerializersModule {
polymorphic(Vehicle::class) {
subclass(Car::class)
subclass(Boat::class)
}
}
and pass it to JSON configuration
val kotlinx = Json {
ignoreUnknownKeys = true
serializersModule = module
}
UPDATE
(Alternative approach with combination of JsonTransformingSerializer & JsonContentPolymorphicSerializer)
Actually, it's possible to combine these two serializers.
For the sake of justification for this approach, let's imagine that original JSON doesn't have that nice kind field, and we have to figure out the actual subtype of Vehicle by the shape of JSON. In this case it could be the following heuristic: "if there is a isDocked field, then it's a Boat, othrewise - a Car".
Yes, we may include this logic into JsonTransformingSerializer to create class descriminator on the fly:
val type = when {
"isDocked" in data -> JsonPrimitive("Boat")
else -> JsonPrimitive("Car")
}
But it's more common (and type-safe) to use JsonContentPolymorphicSerializer for this. So we may simplify JsonTransformingSerializer:
object VehicleResponseSerializer : JsonTransformingSerializer<List<Vehicle>>(ListSerializer(Vehicle.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement {
// equals: [{"kind":"Car","data":{"id":1,"totalWheels":"4"}},{"kind":"Boat","data":{"id":2,"isDocked":true}}]
val vehicleArray = element.jsonObject["children"]!!.jsonArray
// equals: [{"id":1,"totalWheels":"4"}, {"id":2,"isDocked":true}] // Note that class discriminator is absent here!
return JsonArray(vehicleArray.map { it.jsonObject["data"]!! })
}
}
and define JsonContentPolymorphicSerializer (for Vehicle, not for VehicleResponse!) to handle actual serializer selection:
object VehiclePolymorphicSerializer : JsonContentPolymorphicSerializer<Vehicle>(Vehicle::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Vehicle> = when {
"isDocked" in element.jsonObject -> Boat.serializer()
else -> Car.serializer()
}
}
Since JsonTransformingSerializer (aka VehicleResponseSerializer) is registered for vehicles field serialization it's called before JsonContentPolymorphicSerializer (aka VehiclePolymorphicSerializer). Actually, the latter one is not yet called at all. We need to explicitly register it in serializersModule and pass it to JSON configuration (regardless of whether Vehicle class is declared as abstract or sealed):
val module = SerializersModule {
polymorphicDefault(Vehicle::class) { VehiclePolymorphicSerializer }
}
val kotlinx = Json {
ignoreUnknownKeys = true
serializersModule = module
}
I have such JSON:
{
"list": [
{
"product": {
"id": 1123456,
"context": {
}
},
"items": [
]
},
and a code that reads it:
TypeReference<HashMap<String, Object>> typeRef
= new TypeReference<HashMap<String, Object>>() {};
InputStream inputStream = TypeReference.class.getResourceAsStream("/mocks/Docs.json");
Map<String, Object> map = mapper.readValue(inputStream, typeRef);
But I don't want the simple Map<String, Object>, I want to map into a map that looks like Map<String, MyRepresentation> map:
Is there a direct way to do it, or I need first to read it into Map<String, Object> and then manipulate it manually and fill the MyRepresentation object?
the JSON file structure doesn't correspond to the classes at all.
ConditionSummary looks like a type of contextData element accessible as docList[i].product.contextData if it had id, which is defined in the product element.
also, AccountManagerStatistics#map is not public and doesn't have #JsonProperty annotation, so it is out of json for now.
try creating the sample file first if you sure the classes represent the truth:
Map<String, AccountManagerStatistics> map = createStubData();
new ObjectMapper().writerFor(new TypeReference<Map<String, AccountManagerStatistics>>() {}).writeValueAsString(map)
or try to modify your classes to match the data,
which is probably what should be done here.
then you could start from the top and introduce a proper class instead of using Map<String, X>,
BTW no need for HashMap in TypeReference:
//#XmlRootElement
public class Root {
#JsonProperty("docList") //or #XmlElement("docList")
public final List<Doc> docs;
...
#ConstructorProperties({ "docs", ... })
public Root(List<Doc> docs, ...) {
this.docs = List.copyOf(docs);
...
}
}
I have a project with Kotlin and Springboot.
In this project an entity have this following fields: id, name and parent.
What i want is to only get the id and the name. So i made a projection interface view for that entity.
This is my entity:
#Entity
data class Keyword(
#Id
#GeneratedValue(generator = "uuid2")
#GenericGenerator(name = "uuid2", strategy = "uuid2")
#Column(columnDefinition = "BINARY(16)")
var id: UUID? = null,
#Column(columnDefinition = "BINARY(16)")
var parent: UUID? = null,
#NotBlank
var name: String? = null
)
Repository:
#Repository
interface KeywordRepository: JpaRepository<Keyword, UUID> {
#Query(value = "SELECT keyword.id, keyword.parent, keyword.name FROM keyword LEFT JOIN project_keyword ON project_keyword.keyword_id = keyword.id WHERE project_keyword.project_id LIKE :id", nativeQuery = true)
fun findAllKeywordsByProjectId(id: UUID): MutableList<KeyWordView>
}
Service:
#Service
class KeywordService (
private val projectService: ProjectService,
private val keywordRepository: KeywordRepository
) {
fun getKeywordsByProjectId(id: UUID): MutableList<KeyWordView> {
projectService.checkIfProjectExistsById(id)
return keywordRepository.findAllKeywordsByProjectId(id).toMutableList()
}
}
My projection interface class:
interface KeyWordView {
val id: UUID
val name: String?
}
When i call this endpoint via controller class. I get this output:
"list": [
{
"name": "MARINE MICROBIOTA",
"id": "0,31,-36,77,29,123,66,-25,-127,-43,-31,83,104,-90,47,10"
}
]
But if i change the val id: String to val id: UUID in my KeywordView interface, i get this following error:
{
"code": 500,
"data": null,
"message": "Could not write JSON: Projection type must be an interface!; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Projection type must be an interface! (through reference chain: no.inmeta.ris.util.response.ResponseDto[\"data\"]->no.inmeta.ris.util.pagination.PageDto[\"list\"]->java.util.ArrayList[0]->com.sun.proxy.$Proxy199[\"id\"])",
"status": "FAIL"
}
Anyone know how to solve this problem? I want to receive the UUID as UUID not with the strange format.
Thank you!
I guess the problem is the UUID Class. As your entity states you defined dir hibernate that the column in db is a binary(16). And i suppose using this UUID Type in the Interface projection does not Work since there is no valid mapping information (how to transform the data coming as binary with a length of 16 bytes to the UUID Class). So I assume you have to change the Type of your column or you have to use string and write a function to Transform that String.
Another possibility is to create a second entity for that table with Just the two cols you want. And Just use that second dao as your are using yiur projection.
Facing this problem past few days.Does any guys check this issue? Any help would be greatly appreciated.How can I solved this problem?
GSON throwing Expected BEGIN_OBJECT but was BEGIN_ARRAY
Problem coming from
override fun onSuccess(str_SUCCESS: String)
{
System.out.println("JSON_IS"+str_SUCCESS)
val paymentScheduleModel = Gson().fromJson<PaymentScheduleModel>(str_SUCCESS, PaymentScheduleModel::class.java) // Problem here
}
Json Response is
{
"status": {
"statusCode": 10016,
"isSuccess": true,
"message": "Success"
},
"data": {
"payback_schedule": [
{
"id": 2,
"paid_amount": "INR NaN",
"paidStatus": "Upcoming Payback",
"paid_status": "P",
"s_date": "05/01/2018 12:31:10",
"e_date": "11/01/2018 12:31:10",
"current_balance": "INR 399",
"payanytime_button_status": "active",
"btnColor": "red",
"btnHexColor": "#D2322D"
},
{
"id": 3,
"paid_amount": "INR NaN",
"paidStatus": "Upcoming Payback",
"paid_status": "P",
"s_date": "12/01/2018 12:31:10",
"e_date": "18/01/2018 12:31:10",
"current_balance": "INR 399",
"payanytime_button_status": "active",
"btnColor": "red",
"btnHexColor": "#D2322D"
}
]
}
}
PaymentScheduleModel
data class PaymentScheduleModel(#SerializedName("payback_schedule") val payback_schedule: PaymentSchedule)
data class PaymentSchedule
(#SerializedName("id") val id: Int,
#SerializedName("paid_amount") val paid_amount:String,
#SerializedName("paidStatus") val paidStatus:String,
#SerializedName("paid_status") val paid_status:String,
#SerializedName("s_date") val s_date:String,
#SerializedName("e_date") val e_date:String,
#SerializedName("current_balance") val current_balance:String,
#SerializedName("payanytime_button_status") val payanytime_button_status:String,
#SerializedName("btnColor") val btnColor:String,
#SerializedName("btnHexColor") val btnHexColor:String)
Your model object does not match your Json.
You are trying to parse a JsonObject PaymentScheduleModel which has sub object "payback_schedule" of type PaymentSchedule but you have a JsonObject which has a sub object "data" which is what has the sub object "payback_schedule". So really, you want to parse the "data" block.
You have two options:
1: Create another model that wraps the data block and parse that:
data class PaymentScheduleData(#SerializedName("data") val payback_schedule_model: PaymentScheduleModel)
override fun onSuccess(str_SUCCESS: String) {
val paymentScheduleData = Gson().fromJson<PaymentScheduleData>(str_SUCCESS, PaymentScheduleData::class.java)
// Now use paymentScheduleData.payback_schedule_model
}
2: Pull out the data portion first, then parse:
override fun onSuccess(str_SUCCESS: String) {
// Get the root JsonObject
val jsonObject = Gson().fromJson<JsonObject>(str_SUCCESS, JsonObject::class.java)
// Get the "data" block that matches the model and parse that
val paymentScheduleModel = Gson().fromJson<PaymentScheduleModel>(jsonObject.getAsJsonObject("data"), PaymentScheduleModel::class.java)
}
Hope that helps!
The error is telling you that payback_schedule is holding an array instead of object. So, payback_schedule should be Array<PaymentSchedule> instead of PaymentSchedule.
data class PaymentScheduleModel(#SerializedName("payback_schedule") val payback_schedule: Array<PaymentSchedule>)
PS. You are suggested to implement your own equals() and hashCode() function if your data class contains Array because the default implementation of Array's equals() function compares the referential equality. Suggested reading: Equals method for data class in kotlin