I'm trying to send json objects between two Spring Boot services with rpc so for that I need JacksonMessageConverter.
I need send object with field that is an Interface type of multiple DTO classes.
For instance (code in Kotlin):
Response class
class Response(
#JsonProperty("result")
val result: IExampleData?,
#JsonProperty("statusCode")
val statusCode: Int
)
ExampleData classes
interface IExampleData
class ExampleData1(
#JsonProperty("exampleString")
val exampleString: String
) : IExampleData
class ExampleData2(
#JsonProperty("exampleInt")
val exampleInt: Int
) : IExampleData
If I use #JsonDeserialize annotation for one of Example objects, it works fine with correct object:
Response class
...
#JsonDeserialize(`as` = ExampleData1::class)
val result: IExampleData?
...
But it won't work if service send ExampleData2. I need that for all of my objects that implements IExampleData.
I tried to use #JsonSubTypes annotation for result field like that:
...
#JsonSubTypes(
Type(value = ExampleData1::class, name = "exampleData1"),
Type(value = ExampleData2::class, name = "exampleData2"),
)
val result: IExampleData?
...
But it didn't work for me. I get next error:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.example.demo.dto.IExampleData` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
So how can I deserialize one of multiple objects for one field?
Related
Jackson doesn't attempt to decode generic elements of a class.
How do you pass in the the type information without creating a specific version of the class?
The data types are:
#JsonIgnoreProperties(ignoreUnknown = true)
data class HasuraTriggerPayload<T>(
val event: HasuraEvent<T>,
#JsonProperty("created_at")
val createdAt: Instant,
val trigger: Trigger
)
#JsonIgnoreProperties(ignoreUnknown = true)
data class HasuraEvent<T>(
val op: String,
val data: HasuraData<T>
)
data class HasuraData<T>(
val old: T,
val new: T
)
data class Trigger(
val name: String
)
data class Price(
val id: UUID,
val price: BigDecimal?,
)
My route is something like:
post("/price") {
val payload = call.receive<HasuraTriggerPayload<Price>>()
// etc
}
Which produces this error because it doesn't know how to decode new/old:
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.scraptickets.rest.models.Price (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; com.scraptickets.rest.models.Price is in unnamed module of loader 'app')
trigger.name gives the type. in this case, it's "price-trigger"
There are two possible solutions to this.
We know that T is Price at our particular call site, so we could somehow pass along that information, but how?
We can determine the type of old and new from trigger.name, but how do we write that? The only obvious way is from reading a field of the object we're trying to determine the class for, not from outside.
After a bunch of searching, I found the solution.
You need to create a TypeReference.
For my use the solution was:
post("/price") {
val typeRef = object : TypeReference<HasuraTriggerPayload<Price>>() { }
val objectMapper = jacksonObjectMapper() // Not quite this, I'm leaving out the part to handle timestamps
val jsonStr = call.receiveText()
val payload = objectMapper.readValue(jsonStr, typeRef)
// etc
}
I have an interface with two concrete types as part of my model that I'd like to serialize/deserialize with Moshi. My issue is that I don't fully understand if PolymorphicJsonAdapterFactory is actually meant for my use case. I've looked at the samples and a few blog posts and (if I'm understanding them correctly) all of them seem to point to the fact that your interface is supposed to have a field in the interface that allows you to determine the type. I'm working in an existing codebase and so I can't easily add a field that would allow me to figure out what type it is by some string literal.
This is where I'm at with my Moshi code and I'm seeking validation on whether or not I'm using PolymorphicJsonAdapterFactory correctly. Note: I'm using java for the moshi portion of the code and for my model. My interface and it's concrete types are in kotlin
String json = ...;
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(PersonInterface.java, "")
.withSubtype(BusinessPerson.java, "occupation")
.withSubtype(PolicePerson.java, "rank")
)
.build();
JsonAdapter<MyModel> jsonAdapter = moshi.adapter(MyModel.class);
MyModel myModel = jsonAdapter.fromJson(json);
Note: I'm using java for the moshi portion of the code and for my model. My interface and it's concrete types are in kotlin
MyModel is defined as the following
class MyModel {
String month;
PersonInterface person;
}
My interface and concrete classes in kotlin:
Interface PersonInterface {
val personsName: String?
}
data class BusinessPerson(
override val personsName: String,
val occupation: String?
) : PersonInterface
data class PolicePerson(
override val personsName: String,
val rank: String?
) : PersonInterface
The goal is to hopefully have Moshi be able to create a
class MyModel {
String month;
BusinessPerson person;
}
or a
class MyModel {
String month;
PolicePerson person;
}
depending on whether or not the person field contains an occupation (which means it's of type BusinessPerson) or if it contains the field rank (which means it's of type PolicePerson).
PolymorphicJsonAdapterFactory.of(PersonInterface.java, "")
.withSubtype(BusinessPerson.java, "occupation")
.withSubtype(PolicePerson.java, "rank")
means that JSON for every person is supposed to contain a key "" and "occupation" or "rank" is the value for this key, not a key name itself as you want. So e.g. it would encode a
BusinessPerson("John", "CEO")
as
{"": "occupation", "personsName": "John", "occupation": "CEO"}
I think you'll have to write your own adapter factory for this use-case if you want to avoid a discriminator field.
Json to GraphQLArgumetn object conversion failing in graphql-spqr.
tried adding GraphQLInterface(with autodiscovery true and scanpackage) to above abstract classes
and GraphQLtype type all concrete classes.
My graph query:
query contactsQuery($searchQuery : QueryInput) { contacts(searchQuery:$searchQuery){id}}
variables:{"searchQuery":{"bool":{"conditions":[{"must":{"matches":[{"singleFieldMatch":{"boost":null,"field":"firstname","value":"siddiq"}}],"bool":null}}]}})
Java code:
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME,include=JsonTypeInfo.As.WRAPPER_OBJECT)
#JsonSubTypes({#type(value = Must.class, name="must"),#type(value = MustNot.class, name="mustNot")})
public abstract class Condition
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME,include=JsonTypeInfo.As.WRAPPER_OBJECT)
#JsonSubTypes({#type(value = SingleFieldMatch.class, name="singleFieldMatch"),#type(value = MultiFieldMatch.class, name="multiFieldMatch")})
public abstract class Match
#GraphQLQuery(name = "contacts")
public List getContacts(#GraphQLArgument(name ="searchQuery") Query query)
Still it's throwing error unknown field error etc. Not sure which configuration is missing.
Building GraphQLSchema with AnnotatedResolvedBuilder, base package configured JacksonValueMappperFactory and singleton services.
Hi this may be a similar issue to what I ended up having.
Initially I had the following
#JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type")
#GraphQLInterface(name = "AbstractClass", implementationAutoDiscovery = true)
public abstract class AbstractClass{
with the following query called
addNewObject(object: {name: "soft2", id: "asdas"})
To get conversion functioning what I needed to do was the following change
#JsonTypeInfo(use = Id.NAME, include = As.EXISTING_PROPERTY, property = "type")
#GraphQLInterface(name = "AbstractClass", implementationAutoDiscovery = true)
public abstract class AbstractClass{
private String type = this.getClass().getSimpleName();
/**
* #return the type
*/
#GraphQLQuery(name = "type", description = "The concrete type of the node. This should match the initialised class. E.g. \"Concrete\", \"DecafCoffee\"")
public String getType() {
return type;
}
with the query now being
addNewConcreteObject(concrete: {name: "soft2", id: "asdas", type: "Concrete"})
Why this worked (I think):
When converting from JSON to objects in my code using the Jackson converter (ObjectMapper). I had previously noticed that the JSON required knowledge of what class to convert to. Thus the initial use of #JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type") put a type property in the JSON when it was written to string.
The inclusion of the #JSON tag may be picked up by SPQR and it then seems to use a Jackson converter to try to convert your query to the required object.
If I am right, here is the issue.
As the query doesn't contain type the query can not be correctly converted. Moreover as the type property was not a member variable of the object but was instead only added by the ObjectMapper, SPQR didn't pick it up and so it wasn't part of the schema for the object. Thus to get around it, I added type as a member variable which is always equal to the actual class, then changed my JsonTypeInfo to look for an existing property.
I appreciate this isn't a direct answer to your question (and definitely isn't a pretty answer), but hopefully it will help you find your solution.
I'm trying to introduce Kotlin into my current project. I've decided to begin with entities, which seem to map perfectly to data classes.
For example I have a data class:
data class Video(val id: Long, val ownerId: Long, val title: String, val description: String? = null,
val imgLink: String? = null, val created: Date? = null, val accessKey: String? = null,
val views: Long? = null, val comments: Long? = null, val videoLink: String? = null): Entity
Which implements Java interface:
public interface Entity {
Long getId();
}
But for some reason compiler doesn't understand that method is implemented already:
Class 'Video' must be declared abstract or implement abstract member public abstract fun getId(): kotlin.Long! defined in net.alfad.data.Entity
Do I have to use any additional keywords for id param? What does "!" mean in the signature?
The problem here is that Kotlin loads the Java class Entity first and it sees getId as a function, not as a getter of some property. A property getter in a Kotlin class cannot override a function, so the property id is not bound as an implementation of the getId function.
To workaround this, you should override the original function getId in your Kotlin class. Doing so will result in JVM signature clash between your new function and id's getter in the bytecode, so you should also prevent the compiler from generating the getter by making the property private:
data class Video(
private val id: Long,
...
): Entity {
override fun getId() = id
...
}
Note that this answer has been adapted from here: https://stackoverflow.com/a/32971284/288456
If this is your whole data class then you're not overriding getId(). I see that you have a property called id and Kotlin should generate a getter for that but that won't be marked with the override keyword which you need to indicate that you're overriding an abstract function.
-- EDIT --
Alexander beat me to it! His answer is better anyway! ;)
the usual way to serialize to json and back is:
String catStr = mapper.writeValueAsString(cat);
Cat catOut = mapper.readValue(catStr, Cat.class);
Is there a way to add (maybe with annotation ) the type to the json on serialization and let the mapper take the value from it when it deserialize it?
so I can do the following
Object obj = mapper.readValue(catStr);
and later...
Cat catOut = (Cat)obj;
Thanks.
Sort of. You can add a property to the serialization which will indicate what class it is. And then when deserializing it, Jackson deduces the class automatically.
But you cannot deserialize it as Object, you need some base class/interface for all objects you want to behave like this. And that interface needs to use the #JsonTypeInfo annotation, signalling Jackson when deserializing this base class use the property class to distinguish which type.
Example:
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
private abstract class Base {
}
private class A extends Base {
private final int i;
#JsonCreator
public A(#JsonProperty("i") int i) {
this.i = i;
}
}
When serialized will be:
{"#class":"com.test.A","i":3}
Testing code:
A a = new A(3);
String str = mapper.writeValueAsString(a);
Base base = mapper.readValue(str, Base.class);