New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom Serializer - Access Annotation Info #4501
Comments
Can you not use Java reflection APIs to get the annotations from the class and maybe its fields? I don't quite see why Jackson needs to have an API for this when Java provides one out of the box. |
@pjfanning if I am serializing |
Serializers are usually registered for a particular class. For instance, you have Surely, Child has a reference to Parent. |
my original example has the use case data class Parent(val name: String) {
@JsonIgnoreProperties("parent")
var child: Child? = null
}
data class Child(
val name: String,
@JsonIgnoreProperties("child")
val parent: Parent,
) My serializer will handle only serializing val parent = Parent("parent")
val child = Child("child", parent)
parent.child = child
// if I serialize parent.child.parent i get infinite recursion
mapper.writeValueAsString(parent)
// if i serialize child.parent.child i get infinite recursion
mapper.writeValueAsString(child) when serializing the parent, I should get { "name" : "parent", "child": { "name": "child" }} and when serializing the child I should get { "name" : "child", "parent": { "name": "parent" }} |
@austinarbor If infintie recursion is your problem, I think it is resolved in at least 2.17 version. Note that serializing child outputs to class `4501Test` {
data class Parent(val name: String) {
@JsonIgnoreProperties("parent")
var child: Child? = null
}
data class Child(
val name: String,
@JsonIgnoreProperties("child")
val parent: Parent,
)
class ChildSerializer : StdSerializer<Child>(Child::class.java) {
override fun serialize(obj: Child, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
gen.writeFieldName("name")
gen.writeString(obj.name)
gen.writeEndObject()
}
}
@Test
fun `reproduce infinite recursion`() {
// Given
val simpleModule = SimpleModule().addSerializer(Child::class.java, ChildSerializer())
val mapper = jacksonObjectMapper()
.registerModules(simpleModule)
val parent = Parent("parent")
val child = Child("child", parent)
parent.child = child
// When & Then
assertEquals(
"{\"name\":\"parent\",\"child\":{\"name\":\"child\"}}",
mapper.writeValueAsString(parent)
)
assertEquals(
"{\"name\":\"child\"}",
mapper.writeValueAsString(child)
)
}
} |
@JooHyukKim your class ChildSerializer : StdSerializer<Child>(Child::class.java) {
override fun serialize(obj: Child, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
gen.writeFieldName("name")
gen.writeString(obj.name)
gen.writeFieldName("parent")
gen.writeObject(obj.parent)
gen.writeEndObject()
}
} Running that, you get
|
@austinarbor you can implement a ContextualSerializer to access annotations on the property |
What @yawkat said -- Jackson design is such that ALL annotation access must come from "static" part of processing, when constructing and initializing (de)serializers and other handlers: this information is not (and will not) ever be passed during more dynamic serialization/deserialization code path. So information is not and will not be passed through |
@yawkat @cowtowncoder thanks! ContextualSerializer seems to be working well - I did have a little bit of trouble figuring out the best way to "propagate" the annotations to downstream serializers. For example, I am working with Jetbrains' exposed ORM which has a class MyObject {
@JsonIgnoreProperties("someProperty") // the annotation is applied to each item in the list
val others : List<OtherObject> = listOf()
} I wasn't really able to replicate that functionality fully, is there a better approach to use than class LazySizedCollectionSerializer(private val bean: BeanProperty? = null) :
StdSerializer<LazySizedCollection<*>>(
LazySizedCollection::class.java,
),
ContextualSerializer {
override fun serialize(
value: LazySizedCollection<*>,
gen: JsonGenerator,
provider: SerializerProvider,
) {
gen.writeStartArray()
// if the value is loaded from the database already, serialize it
// otherwise just write an empty array
if (value.isLoaded()) {
for (item in value) {
if (item == null) {
gen.writeNull()
} else {
// for some reason, the calls to _other_ createContextual custom serializers
// all were passing a null bean property, even with this below
val ser = provider.findValueSerializer(item::class.java, bean)
ser.serialize(item, gen, provider)
}
}
}
gen.writeEndArray()
}
override fun createContextual(
prov: SerializerProvider?,
property: BeanProperty?,
): JsonSerializer<*> {
// the bean property here contains the JsonIgnoreProperties annotation information
return LazySizedCollectionSerializer(property)
}
} |
Hmmh. Yeah, that is challenging thing to do; given that handling of elements (applying annotations to collection elements), it might make sense to instead use And implementing custom List serializer it might make sense to extend a more advanced base class than src/main/java/com/fasterxml/jackson/databind/ser/impl/IndexedListSerializer.java extends Hopefully some combination works. |
The class SerializerModifier : BeanSerializerModifier() {
override fun modifySerializer(
config: SerializationConfig,
beanDesc: BeanDescription,
serializer: JsonSerializer<*>,
): JsonSerializer<*> {
if (beanDesc.beanClass.kotlin.isSubclassOf(Entity::class)) {
return EntitySerializer(serializer as JsonSerializer<Any>)
}
return serializer
}
}
class EntitySerializer(private val ser: JsonSerializer<Any>, private val bean: BeanProperty? = null)
: StdSerializer<Entity<*>>(Entity::class.java), ContextualSerializer {
private val ignoredProperties: Set<String>
init {
val ignored = mutableSetOf<String>()
if (bean != null) {
// the javadoc says to use the AnnotationInspector, but the ignored properties never change so
// i thought it would be more efficient to calculate them once up front
bean.getAnnotation(JsonIgnoreProperties::class.java)?.value?.forEach { ignored.add(it) }
bean.getContextAnnotation(JsonIgnoreProperties::class.java)?.value?.forEach {
ignored.add(it)
}
}
ignoredProperties = ignored
}
override fun serialize(entity: Entity<*>, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
// right now this code looks like it could be replaced by the default serializer,
// but i am excluding some custom logic for brevity. i just want to demonstrate how
// i am determining the ignored properties
ser.properties()
.asSequence()
.filterNot { it.name in ignoredProperties }
.forEach { prop ->
gen.writeFieldName(prop.name)
val v = prop.member.getValue(entity)
if (v == null) {
gen.writeNull()
} else {
val valueSerializer = provider.findValueSerializer(v::class.java, prop)
valueSerializer.serialize(v, gen, provider)
}
}
gen.writeEndObject()
}
override fun createContextual(prov: SerializerProvider, property: BeanProperty?): JsonSerializer<*> {
return EntitySerializer(this.ser, property)
}
}
class LazySizedCollectionSerializer(private val bean: BeanProperty? = null) :
StdSerializer<LazySizedCollection<*>>(LazySizedCollection::class.java), ContextualSerializer {
override fun serialize(
value: LazySizedCollection<*>,
gen: JsonGenerator,
provider: SerializerProvider,
) {
if (value.isLoaded()) {
val ser = provider.findValueSerializer(List::class.java, bean)
ser.serialize(value.wrapper, gen, provider)
} else {
// value was not specified to be eagerly fetched from the database so write null
gen.writeNull()
}
}
override fun createContextual(provider: SerializerProvider?, property: BeanProperty?): JsonSerializer<*> {
return LazySizedCollectionSerializer(property)
}
} |
That looks fine overall, except this:
is probably not what you want: it'd be for class annotations of class that has property with Actually... another thing worth considering is that And from performance perspective, having to dynamically call Oh, one more thing:
so you can apply ignorals (serializers for which these do not apply, like scalar serializers, simply do Anyway, I hope some of above is useful. Good progress so far! :) |
Hmm... Agreed this probably isn't the most performant - I opened a ticket on the Exposed YouTrack to add a feature which would let me optimize a bit more The only reason I need the custom serializer is because I need to wrap the getValue call in a try/catch and ignore a specific exception and write null when it happens. Exposed currently doesn't have a way to checking if a 1:1 association has been fetched yet, and if you try and fetch the association after the transaction is closed you get an exception. It's not pretty, but at the moment it's the only way to get it done (that I know of). If there's an easier way to implement writing a ser.properties()
.asSequence()
.filterNot { it.name in ignoredProperties }
.forEach { prop ->
gen.writeFieldName(prop.name)
try {
val v = prop.member.getValue(entity)
if (v == null) {
gen.writeNull()
} else {
val valueSerializer = provider.findValueSerializer(v::class.java, prop)
valueSerializer.serialize(v, gen, provider)
}
} catch(e: IllegalArgumentException) {
// if we get an IllegalArgumentException here with a certain underlying cause, it means
// the entity's association was not fetched and we want to just write null to the json
gen.writeNull()
}
} I wonder...can I provide a custom Introspector for one class only that provides BeanProperty with a wrapped |
There is no way to change Odd that |
Is your feature request related to a problem? Please describe.
When writing a custom serializer, I'd like to be able to know the Jackson annotations that were on the entity being serialized. Serializing fields may be be intensive or cause recursion, so if the caller has specified to ignore that field, I would like to know before I try and serialize it
Describe the solution you'd like
Either
JsonGenerator
orSerializerProvider
(or some other static method) should provide a lookup mechanism to get the Jackson annotations related to the current object being serializedUsage example
Additional context
No response
The text was updated successfully, but these errors were encountered: