Casting an object to limit data serialized in Jackson serialization - java

Let's say I have the following classes:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "type",
visible = true
)
#JsonSubTypes(
JsonSubTypes.Type(FullXResult::class, name = "X")
)
open class BaseResult {
protected val mapper: ObjectMapper = ObjectMapper()
var x: String = "some shared text between all subclasses"
}
open class MinimalXResult: BaseResult() {
var y: String = "some public text specific to X"
}
class FullXResult: MinimalXResult() {
var z: String = "some private text specific to X"
fun asMinimal(): MinimalXResult {
return mapper.convertValue(this, MinimalXResult::class.java)
}
}
I want to be able to deserialize a JSON into the FullResult, and before re-serialization, cast it to a MinimalResult to the caller such that for the example classes, the properties x and y are serialized but the property z is not. #JsonIgnore is not an option here because I also want to be able to return the full result under certain conditions.
When calling asMinimal function, I get the following error:
Could not resolve type id 'X' as a subtype of `MinimalXResult`: Class `FullXResult` not subtype of `MinimalXResult`
If this type of casting can be done without Jackson, that is fine too. Thanks for any help.

Related

I have an Pojo Kotlin Object which contains list, child objects and sub child objects and I need to change the value of a field using field name

Example given classes as below
data class Book (
var name: String? = null,
var codes: List<Code>? = null,
var description: Description? = null
)
data class Code(
var value: String? = null
var date: String? = null
)
data class Description(
var notes: String? = null
)
I am looking a util where I can change the value of the given field name.
For example: I need to change the date value of Code class using field name "date". I am looking at generic util class which we can use to change the value at any given child class or list or map....which matches the give string name
I do not know of any existing utils for that purpose.
A very simple method you could use would be a Jackson object mapper:
inline fun <reified T: Any> setProperty(c: T, propName: String, value: Any?): T {
val map = jacksonObjectMapper().convertValue<Map<String, Any?>>(c).toMutableMap()
map[propName] = value
return jacksonObjectMapper().convertValue(map)
}
Then you can for instance write the following to set the date value of a Code object:
val code = Code("x", "y")
val resultCode = setProperty(code, "date", "z")
But please note that this has very bad performance since it converts the data class to json, then to a map, then again to json, and finally back into the data class. It is also not very good because it creates a new object each time you change one property.
A bit better would be to use Kotlin reflection like this:
import kotlin.reflect.KMutableProperty
import kotlin.reflect.full.memberProperties
fun <T : Any> setProp(c: T, propName: String, value: Any?) {
val prop = getMutableProperty(c, propName)
val type = prop.setter.parameters[1].type
if (value == null && !type.isMarkedNullable) error("property $propName is not nullable")
prop.setter.call(c, value)
}
fun <T: Any> getMutableProperty(c: T, propName: String): KMutableProperty<*> {
val prop = c::class.memberProperties.find { it.name == propName } ?: error("no property $propName")
if (prop !is KMutableProperty<*>) error("property $propName is not mutable")
return prop
}
This can be used to change the property of a data class without creating a new element on each changed property which would look like this:
val code = Code("x", "y")
setProp(code, "date", "myval")
println(code) // now prints Code(value=x, date=myval)

Decode generic type with Jackson

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
}

Json to object deserialization issue in Graphql-spqr

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.

How to use setter for this Kotlin class property?

I have the following class coded in Kotlin:
class MyClass {
var color: String = ""
var action: String = ""
val owners = Array(1) {Owner()}
class Owner {
var userId: String = ""
var userName: String = ""
}
}
...and I'm accessing it Java:
MyClass myObject = new MyClass();
myObject.setColor("blue");
myObject.setAction("throw");
...and I'd like to be able to set the owner. I'm not sure how, though. If it were an object that was coded in Java with public members, I'd just do something like:
myObject.owners[0].userId = "001";
myObject.owners[0].userName = "Freddy"
Since the object was coded in Kotlin, I need to user a setter in Java.
How do I set the properties in the first element of an array with a setter?
For each Kotlin property foo, you can call its getter in Java as getFoo() and, if the property is mutable, the setter as setFoo(value).
See: Calling Kotlin from Java — Properties
In your case, just access the array with the getter, take its item and call the setters: myObject.getOwners()[0].setUserId("001"); and myObject.getOwners()[0].setUserName("Freddy");, or assign the Owner to a local variable:
MyClass.Owner owner = myObject.getOwners()[0];
owner.setUserId("001");
owner.setUserName("Freddy");
Use getOwners which will return owners object then set the value.
myObject.getOwners()[0].setUserId("001");
myObject.getOwners()[0].setUserName("Freddy");

Java Jackson : Can I hold in a json the type to deserialize it to?

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);

Categories

Resources