I have the following generic sealed class representing the status of network response.
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "status")
#JsonSubTypes(
value = [
JsonSubTypes.Type(value = Response.OK::class, name = "OK"),
JsonSubTypes.Type(value = Response.Error::class, name = "ERROR"),
]
)
sealed class Response<out Content, out Error> {
data class OK<out Content>(val content: Content) : Response<Content, Nothing>()
data class Error<out Error>(val error: Error) : Response<Nothing, Error>()
}
The network response json can have status": "OK" in which case it contains a content key or a "status": "ERROR" in which case it contains a error key.
The type under the content and error key can be different for each endpoint I’m talking to. Hence the need for generic types
So for example one endpoit returns String as types, so Response<String, String>
{
"status": "OK",
"content": "Hello"
}
{
"status": "ERROR",
"error": "MyError"
}
Another endpoit return Double as content and Int as error, so
Response<Double, Int>
{
"status": "OK",
"content": 2.0
}
{
"status": "ERROR",
"error": 1
}
My parsing fails though with message
Could not resolve type id 'OK' as a subtype of `com.example.models.Response<java.lang.String,java.lang.String>`: Failed to specialize base type com.example.models.Response<java.lang.String,java.lang.String> as com.example.models.Response$OK, problem: Type parameter #1/2 differs; can not specialize java.lang.String with java.lang.Object
at [Source: (String)"{
"status": "OK",
"content": "Hello"
}"; line: 2, column: 13]
#Nested
inner class ContentParsingTests {
#Test
fun `parses OK response`() {
val json = """
{
"status": "OK",
"content": "Hello"
}
""".trimIndent()
when (val result = objectMapper.readValue<Response<String, String>>(json)) {
is Response.OK -> {
assertEquals(result.content, "Hello")
}
is Response.Error -> {
fail()
}
}
}
#Test
fun `parses ERROR response`() {
val json = """
{
"status": "ERROR",
"error": "MyError"
}
""".trimIndent()
when (val result = objectMapper.readValue<Response<String, String>>(json)) {
is Response.OK -> {
fail()
}
is Response.Error -> {
assertEquals(result.error, "MyError")
}
}
}
}
I noticed that the parsing works fine if only the content is generic:
sealed class Response<out Content > {
data class OK<out Content>(val content: Content) : Response<Content>()
object Error : Response<Nothing>()
}
but of course I loose the error payload
What would be a correct way to parse the json into my generic class?
I think the issue is with Nothing because it's like Void and you can't create an instance of it or get a type information that's why the serialization library struggling with it. so a solution for the current problem is to update the model definition like this and it works. It's not ideal though.
sealed class Response<out Content, out Error> {
data class OK<out Content, out Error>(val content: Content) : Response<Content, Error>()
data class Error<out Content, out Error>(val error: Error) : Response<Content, Error>()
}
You don't need generics at all for this case. Just have:
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "status")
#JsonSubTypes(
value = [
JsonSubTypes.Type(value = Response.OK::class, name = "OK"),
JsonSubTypes.Type(value = Response.Error::class, name = "ERROR"),
]
)
sealed interface Response {
data class Success(val content: Content): Response
data class Error(val error: Error): Response
}
Jackson will then be able to parse everything correctly.
Related
Having a function for deserializing json string into object mode by providing the class type the json string should be deserialized into.
It works for most of the case
fun <T> deserializeJsonStr(jsonString: String, dataClassType: Class<T>):T? {
var ret: T? = Gson().fromJson<T>(jsonString, dataClassType)
return ret
}
like
// data modle
class TestData1 {
var userId = 0
var id = 0
var title: String? = null
var body: String? = null
}
// the json string
{
"userId": 1,
"id": 3,
"title": "the title",
"body": "the body"
}
// call the function
val clazz: Class<TestData1> = TestData1::class.java
val theTestData1 = deserializeJsonStr(jsonString, clazz)
it will return the object theTestData1 with the fields filled.
But if the json string is for json array:
[
{
"userId": 1,
"id": 1,
"title": "title1",
"body": "body1"
},
{
"userId": 1,
"id": 2,
"title": "title2",
"body": "body2"
},
{
"userId": 1,
"id": 3,
"title": "title3",
"body": "body3"
}
]
Though it should be ArrayList<TestData1>, but what is it class type? tried:
val clazz: Class<ArrayList<TestData1>> = ArrayList<TestData1>::class.java
val theTestData1 = psreJsonStr(jsonString, clazz)
but it does not compile:
or the val pojoClass: Class<ArrayList<TestData1>> = ArrayList<>::class.java does not compile either:
what would be the data class mode for this json array? or what is the Class<T> required by the function param for ArrayList?
This is related to type erasure: ArrayList<TestData1>::class.java is not possible because at runtime the type argument of ArrayList is not available, so this would basically be Class<ArrayList>1. Therefore the compiler does not allow this because it would not be safe.
Gson has its TypeToken class to work around this, see also the user guide.
In your case you could change your deserializeJsonStr to the following:
fun <T> deserializeJsonStr(jsonString: String, dataType: TypeToken<T>): T? {
// Note: Older Gson versions might require using `dataType.type`
return Gson().fromJson(jsonString, dataType)
}
You would then call the function like this:
val type = object: TypeToken<ArrayList<TestData1>>() {}
val data = deserializeJsonStr(jsonString, type)
You could additionally add an overload of deserializeJsonStr with reified type parameter which makes it make more convenient to use:
inline fun <reified T> deserializeJsonStr(jsonString: String): T? {
return deserializeJsonStr(jsonString, object: TypeToken<T>() {})
}
// Note: You can even omit the explicit `ArrayList<TestData1>` if the compiler can infer it
val data = deserializeJsonStr<ArrayList<TestData1>>(jsonString, type)
1 Class is a bit special because its direct type argument is not erased at runtime, for example Class<ArrayList> is not erased to Class<Object>. But for every other generic type the type argument is erased.
My objectmapper.readValue() function throws an error that says "Cannot construct instance of MyErrorMessage (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value."
What's wrong with my MyErrorMessage class that makes objectmapper fail?
This is the JSON I'm trying to parse.
{
"meta": {
"id": "43225a4853b5497a",
"time": "2020-06-03T13:36:03.391814Z"
},
"datadetail": {
"aKey": "hweriu-erw",
"aTypes": [
{
"bKey": "ewrf-7e9f",
"cKey": "12ddf3",
"status": "ERROR",
"errorMessage": {
"message": "Not found"
}
}
],
"status": "ONE",
"errorMessage": "ERROR with aKey"
}
}
This is the function and the classes.
private fun ParseResponse(responseMessage: String): MyResponse{
try {
val objectMapper = ObjectMapper()
return objectmapper.readValue(message, MyResponse::class.java)
}catch (e: JsonProcessingException) {
throw IllegalArgumentException("json was invalid $responseMessage", e)
}
}
data class MyResponse(
val meta: Metainfo,
val datadetail: DataResponse
)
data class MetaInfo(
val id: String,
val time: Instant
)
data class DataResponse(
val aKey: MyKey,
val aTypes: List<TypesResponse>,
val aNumber: String? = null,
val status: StatusType,
val errorMessage: MyErrorMessage = null
)
enum class StatusType{
OK,
ERROR
}
data class TypesResponse(
val bKey: MyBKey,
val cKey: MyCKey,
val status: StatusType,
val errMessage: MyErrorMessage? = null
)
data class MyErrorMessage(
#JsonProperty("message")
val message: String,
#JsonProperty("context")
val context: MyContext?,
){
constructor(message: String) : this(
message = message,
context = null
)
enum class MyContext{
ONE, TWO, THREE
}
}
Your JSON does not follow your class structure.
You have "errorMessage": "ERROR with aKey", but your errorMessage in DataResponse is in fact a MyErrorMessage which is an object, not a simple String.
It would need to be:
"errorMessage": {
"message": "ERROR with aKey"
}
If that is not an option, then you need a custom Jackson JSON Deserializer for your MyErrorMessage that can handle that. You can check an example here.
I have an abstract class "Elem" with a bunch of children (TXT, IMG, EDT ...). They all have contructors.
I need to parse in json an Object contaning a list of children of an abstract class
abstract class Elem(
var content : String,
var condition : String = ""
){
open fun instantiate(){
}
}
class TXT(content: String) : Elem(content) {
override fun instantiate() {
//Some work
}
}
class BTN(content: String, private val additional : String) : Elem(content) {
override fun instantiate() {
//Some work
}
}
...
EDIT :
I tried to used the AbstractElementAdapter, as shown here
Here the new code to parse a JSON
val e = MyObject(/*Some stuff,*/listOf(TXT("Hello"), IMG("world.png"))))
val gsonBuilder = GsonBuilder()
gsonBuilder.registerTypeAdapter(EtapElem::class.java, JsonSerializer<EtapElem>{
src, _, context ->
val result = JsonObject()
result.add("type", JsonPrimitive(src.javaClass.simpleName))
result.add("elem", context.serialize(src, src.javaClass))
return#JsonSerializer result
})
val jsonPretty: String = gsonBuilder .setPrettyPrinting().create().toJson(e)
The json looks fine
{
//Some stuff,
"elems": [
{
"type": "TXT",
"elem": {
"content": "Hello?",
"condition": ""
}
},
{
"type": "IMG",
"elem": {
"content": "world.png",
"condition": ""
}
}
]
}
Now the read :
val gsonBuilder = GsonBuilder()
gsonBuilder.registerTypeAdapter(EtapElem::class.java, JsonDeserializer<EtapElem>{
json, _, context ->
val jsonObject = json.asJsonObject
val type = jsonObject["type"].asString
val element = jsonObject["elem"]
return#JsonDeserializer try {
context.deserialize(element, Class.forName("com.package.path.elem.$type"))
} catch (cnfe: ClassNotFoundException) {
throw JsonParseException("Unknown element type: $type", cnfe)
}
})
val outtype = object : TypeToken<MyObject>() {}.type
val s : Scenario = gsonBuilder.create().fromJson(jsonFileString, outtype)
I have an exception thrown in read :
java.lang.ClassCastException: class com.package.path.elem.TXT cannot be cast to class java.lang.Void (com.package.path.elem.TXT is in unnamed module of loader 'app'; java.lang.Void is in module java.base of loader 'bootstrap')
The JSON file I'm pulling from unfortunately has a node with the same variable name but could have two different data types randomly. When I make a network call (using gson) I get the error:
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a BEGIN_ARRAY but was int at line 1 column 5344 path $[1].medium
the JSON looks like
{
"title": "Live JSON generator",
"url": google.com,
"medium": ["chicken", "radio", "room"]
}
//However sometimes medium can be:
"medium": 259
My Serialized class looks like:
data class SearchItem(
#SerializedName("title") var title: String,
#SerializedName("url") var urlStr: String,
#SerializedName("medium") val medium: List<String>? = null
) : Serializable {}
The way I'm making the network call is like this:
private val api: P1Api
fun onItemClicked(searchItem: SearchItem) {
api.getCollections { response, error ->
response.toString()
val searchItems: List<SearchItem> = Util.gson?.fromJson<List<SearchItem>>(
response.get("results").toString()
, object : TypeToken<List<SearchItem>>() {}.type)?.toList()!!
...
doStuffWithSearchItems(searchItems)
}
How do I handle both cases where "medium" can either be an array of strings or it could be an Int?
You could write custom JsonDeserializer for this case:
class SearchItemCustomDeserializer: JsonDeserializer<SearchItem> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): SearchItem {
val obj = json.asJsonObject
val title = obj.get("title").asString
val url = obj.get("url").asString
val mediumProp = obj.get("medium")
val medium = if(mediumProp.isJsonArray) {
mediumProp.asJsonArray.map { it.asString }
} else {
listOf(mediumProp.asString)
}
return SearchItem(
title = title,
urlStr = url,
medium = medium
)
}
}
With this class you "manually" deserialize json to object. For medium property we check is this array or simple json primitive with function mediumProp.isJsonArray. And if answer is yes - then deserialize field as json array of strings mediumProp.asJsonArray.map { it.asString } Else deserialize the field as string.
And then we register our custom SearchItemCustomDeserializer on GsonBuilder using method registerTypeAdapter
val gson = GsonBuilder()
.registerTypeAdapter(SearchItem::class.java, SearchItemCustomDeserializer())
.create()
And after this you can use this gson instance to deserialize yours objects
I'm trying to deserialize the following JSON:
{
"listings": {
"-L19C5OjcDSjMi4-oha-": {
"listing_id": "-L19C5OjcDSjMi4-oha-",
"location": "Edinburgh"
},
"-L19CJrzEpChO_W14YkC": {
"listing_id": "-L19CJrzEpChO_W14YkC",
"location": "Edinburgh",
"messages": {
"Rp7ytJdEvZeMFgpLqeCSzkSeTyf1": {
"-L19V4QpPMCMwGcNaQBG": {
"senderId": "Rp7ytJdEvZeMFgpLqeCSzkSeTyf1",
"senderName": "Albert",
"text": "Hey there"
},
"-L19r0osoet4f9SjBGE7": {
"senderId": "YMM45tgFFvYB7rx9PhC2TE5eW6D2",
"senderName": "David",
"text": "Hi"
}
}
}
},
"-L19ChjPjX1DnfQb28AW": {
"listing_id": "-L19ChjPjX1DnfQb28AW",
"location": "Edinburgh",
"messages": {
"879dUqGuiXSd95QHzfhbSs05IZn2": {
"-L1i6c7sGf3BcF2cCSCu": {
"senderId": "879dUqGuiXSd95QHzfhbSs05IZn2",
"senderName": "Alberto",
"text": "Hello"
}
},
"Rp7ytJdEvZeMFgpLqeCSzkSeTyf1": {
"-L19FGCMuQACjYKCFEwV": {
"senderId": "Rp7ytJdEvZeMFgpLqeCSzkSeTyf1",
"senderName": "Albert",
"text": "Hey"
},
"-L19T_v2Utxhu1mGhz7-": {
"senderId": "YMM45tgFFvYB7rx9PhC2TE5eW6D2",
"senderName": "David",
"text": "Hi"
},
"-L19TbhActGmga4f47Mz": {
"senderId": "Rp7ytJdEvZeMFgpLqeCSzkSeTyf1",
"senderName": "Albert",
"text": "How are you"
}
}
}
},
"-L19Cz1abm1o-JCbiAnN": {
"listing_id": "-L19Cz1abm1o-JCbiAnN",
"location": "Edinburgh"
},
"-L19DMdFx2pXj9-EKCq2": {
"listing_id": "-L19DMdFx2pXj9-EKCq2",
"location": "Edinburgh"
},
"-L19DV67WjguozFE_4dM": {
"listing_id": "-L19DV67WjguozFE_4dM",
"location": "Edinburgh"
}
}
}
In order to do so I have created the following records:
type MessageContent =
{ senderId: string
senderName: string
text: string; }
type Message =
{ timestampId : string
chatMessages : MessageContent;}
type Chat =
{ chatPartnerId : string
Messages : Message array;}
type ListingContent =
{ from : string
landlord_id : string
listing_id : string
location : string
name : string
pic_1_url : string
pic_2_url : string
pic_3_url : string
pic_4_url : string
pic_5_url : string
messages : Chat array
postcode : string
price_per_night : int
to_date : string;
}
type Listing =
{ timestampId : string
chatMessages : ListingContent;}
type City =
{ city : string
listings : Listing array
}
type AllListings =
{ cities : City array;}
type SearchSettings =
{ from : string
location : string
max_price : decimal
min_price : decimal
to_date : string;}
type MatchContent =
{ id : string
location : string;}
type Match =
{timestampId : string
matchContent : MatchContent;}
type DeclinedContent =
{ id : string;
}
type Declined =
{timestampId : string
declinedContent : DeclinedContent;}
type ListingUserContent =
{ listing_id : string
location : string
messages : Chat array;
}
type ListingUser =
{timestampId : string
listingUser : ListingUserContent;}
type UserContent =
{ declined: Declined array
matches : Match array
searchSettings : SearchSettings
user_listings : ListingUser array;
}
Next, I have the following line of code:
let listings = JsonConvert.DeserializeObject<Types.UserContent>(html)
where html is the JSON string shown above.
However, this throws the following error:
Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'Types+Declined[]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.
To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List<T>) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.
Path 'declined.-L0tmKVgUcj_a1ubO5Zd', line 1, position 36.
I believe this might because there is no Declined in this particular JSON, however all of the 4 members of the UserContent Record are completely optional (they might all be there, or none of them might be there)...is this what I'm doing wrong? If so, how do I fix it, and allow for optional values.
UPDATE:
So I commented out the code which actually does the deserialization and I'm still getting the weird error, I don't think its related to my code
Make the members of your UserContent record Option types, and add a TypeConverter for F# Option types, such as this one, to your JSON Serialization Settings.
type UserContent =
{ declined: Declined array option
matches : Match array option
searchSettings : SearchSettings option
user_listings : ListingUser array option;
}
type OptionConverter() =
inherit JsonConverter()
override __.CanConvert(t) =
t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<option<_>>
override __.WriteJson(writer, value, serializer) =
let value =
if value |> isNull
then null
else let _,fields = FSharpValue.GetUnionFields(value, value.GetType())
fields.[0]
serializer.Serialize(writer, value)
override __.ReadJson(reader, t, existingValue, serializer) =
let innerType = t.GetGenericArguments().[0]
let innerType =
if innerType.IsValueType
then (typedefof<Nullable<_>>).MakeGenericType([|innerType|])
else innerType
let value = serializer.Deserialize(reader, innerType)
let cases = FSharpType.GetUnionCases(t)
if value |> isNull
then FSharpValue.MakeUnion(cases.[0], [||])
else FSharpValue.MakeUnion(cases.[1], [|value|])
let serializer = JsonSerializer.Create(JsonSerializerSettings(Converters = [| OptionConverter() |]))
use stringReader = new StringReader(html)
use jsonReader = new JsonTextReader(stringReader)
serializer.Deserialize<Types.UserContent>(jsonReader)