ObjectMapper can't handle mapping object with legacy enums (classes) - java

I'm having an issue with building REST architecture for some legacy code.
Jackson ObjectMapper is unable to map my custom object to legacy object, because of 'enums' that really are classes with static final fields.
I tried implementing custom converters/deserializers with no success
In the legacy system there are enums that look like this:
public final class LegacyEnum extends LegacyEnumSuperclass {
public static final LegacyEnum VALUE = new LegacyEnum("1");
I'm receiving values of these 'enums' as Strings, that I convert to legacy enum values (custom deserializer) and set them in my custom class (I need it because I'm using jackson annotations, and I have no access or permission to modify legacy code) and this part works nicely. When I try to map my custom object to legacy object with
objectMapper.convertValue(myCustomObject, LegacyObjectContainingEnums.class);
I get an exception:
Can not construct instance of LegacyEnum: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)
The LegacyEnum class has a private constructor, and LegacyEnumSuperclass has a similar protected constructor so I cannot access them (neither can ObjectMapper). I have tried implementing a custom converter, that would skip the 'create new object' part of ObjectMapper mapping, and I also tried to reuse my custom deserializer. I ran into multiple issues and achieved no success.
The most annoying part is, that when I use ModelMapper library it works like a charm (it probably just sets a value in the legacy object, no need to create new LegacyEnum instance like ObjectMapper!) but I'm trying to resolve that issue without adding new dependencies.
Any ideas?

I resolved the issue by using MixIn and custom deserializer, like this:
public abstract class LegacyClassMixIn
#JsonDeserialize(using = LegacyEnumDeserializer.class)
abstract LegacyEnum getLegacyEnum();
Deserializer:
public class LegacyEnumDeserializer extends JsonDeserializer<LegacyEnumSuperclass> implements ContextualDeserializer {
private JavaType valueType;
#Override
public JsonDeserializer createContextual(DeserializationContext context, BeanProperty property) {
JavaType wrapperType = property.getType();
LegacyEnumDeserializer deserializer = new LegacyEnumDeserializer();
deserializer.valueType = wrapperType;
return deserializer;
}
#Override
public LegacyEnumSuperclass deserialize(JsonParser parser, DeserializationContext context) throws IOException {
return LegacyEnumSuperclass.getEnum(valueType.getRawClass(), parser.readValueAs(String.class));
}
valueType.getRawClass() returns LegacyEnum.class, that way I can use one deserializer for all of the 'enums' that inherit LegacyEnumSuperclass class. getEnum is a custom method, from the legacy code.
Registering MixIn in ObjectMapper Spring configuration:
#Configuration
public class ObjectMapperConfig {
public ObjectMapperConfig(ObjectMapper objectMapper) {
objectMapper.addMixIn(LegacyClass.class, LegacyClassMixIn.class);
}
}
That way I can use LegacyClass as a parameter in a Controller method.
Thanks for clues.

Related

Kafka JSON Deserializer for interfaces

I've got problem similar to this:
Kafka Deserialize Nested Generic Types
In my kafka producer I am sending object that looks like this:
public class ExternalTO implements Serializable
{
private static final long serialVersionUID = 7949808917892350503L;
private List<IExternalData> externalDatas;
public ExternalTO()
{}
}
The cornerstone is this: List<IExternalData> externalDatas.
This interface looks like:
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public interface IExternalData
{
String getOne();
}
In my application there can by generated multiple types of IExternalBetData interface implementations (about 10 different). In this case, for instance, my producer generated ExternalTO with inner list of ConcreteExternalData objects. Sent JSON looks like:
{
"externalDatas":
[{"#class":"com.api.external.to.ConcreteExternalData",
"one":false,
"two":false}]
}
Field #class was added because of #JsonTypeInfo annotation, and I thought that this is enough for deserializer to "understend" what type of IExternalData to use in deserialization.
Unfortunately, on the side of kafka listener I am getting the exception:
Cannot construct instance of com.api.external.to.IExternalData (no
Creators, like default construct, exist): abstract types either need
to be mapped to concrete types, have custom deserializer, or contain
additional type information
Consumer looks similar to:
#Service
public class Consumer
{
private final ObjectMapper objectMapper;
public Consumer(ObjectMapper objectMapper)
{
this.objectMapper = objectMapper;
}
#KafkaListener(topics = {"${kafka.topic}"})
public void listen(ConsumerRecord<String, String> record)
{
objectMapper.readValue(record.value(), ExternalTO.class)
}
Please, help to solve this issue with deseriatization.
The deserializer doesn't know, out of all the implementations of IExternalData, to which it should deserialize the consumer record data to. We must resolve that ambiguity.
I was able to resolve this using #JsonDeserialize annotation.
#JsonDeserialize(as = <Implementation>.class
above the declaration of the List
The solution for me was to set property to objectMapper.
ObjectMapper mapper = new ObjectMapper();
// deserializes IExternalData into certain implementation.
mapper.enableDefaultTyping();

POJO deserialization property of interface type withoud impementation class with Jackson

I have a problem with deserialization json into POJO class which looks like this:
#Data
public class Foo {
private String fieldA;
private String fieldB;
private IBar fieldC;
}
IBar is an interface which defines getters for some classes. One of the solutions what I found is to use #JsonDeserialize(as = BarImpl.class) where BarImpl will implement IBar interface. Problem is classes which implement that interface (for instance BarImpl) are in another maven module where I don't have access from current module so I cannot use one of this impl classes in that annotation. Can you tell me if there is another solution?
Thank you in advice.
Are you sure you mean deserialization? You'll need a concrete implementation of your interface if Jackson's going to be able to create Java objects for you.
deserialization = Json String -> Java object
serialization = Java object -> Json String
When serializing Jackson will use the runtime class of the object, so it will use the actual implementations rather than attempt to use the interface. If you want to customize this you can add a serializer for your interface. You'll need to decide exactly what you want to write out.
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(IBar.class, new JsonSerializer<IBar>() {
#Override
public void serialize(IBar value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeStringField("fieldName", value.getFieldName());
gen.writeEndObject();
}
});
objectMapper.registerModule(module);

Is there a way to customize serialization of only one specific String typed field of a JPA entity in a Spring Boot app?

I only want custom serialization on a specific String typed field of a JPA entity. For example a field named details on an entity class named User. The serialization logic needs access to some other Spring beans so those beans should be injected to it.
This JPA object might be the root of serialization or a member of another entity that is to be serialized.
So I think I cannot use com.fasterxml.jackson.databind.annotation.JsonSerialize. If I define a org.springframework.boot.jackson.JsonComponent for the type String, it will be invoked for all String fields and can have potential negative side effects on the serialization performance/throughput.
Is there a solution for this problem?
Here is a custom serializer for a String field. In this simple example it just makes it lowercase
class DetailsSerializer extends StdSerializer<String> {
public DetailsSerializer() { this(null); }
public DetailsSerializer(Class<String> t) { super(t); }
#Override
public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(value.toLowerCase());
}
}
The important bit is to annotate the particular field so that this serializer is used for this field only. E.g.
class User {
#JsonSerialize(using = DetailsSerializer.class)
private final String details;
// other fields getters etc.
}
EntityListeners are eligible for injection, you can declare a callback for the load event in the listener and pass whatever bean you need to the entity
public class MyListener {
#Inject MyBean bean;
#PostLoad
public void inject(MyEntity entity) {
entity.bean = bean;
}
}
And your entity:
#Entity
#EntityListeners(MyListener.class)
public class MyEntity {
public #Transient MyBean bean;
}
You probably have annotated your deserializer with #JsonComponent, which makes Jackson have it the default deserializer for every String.
Remove that annotation, and annotate the fields you want deserialized with that custom deserializer, with #JsonDeserializer(using = MyDeserializer.class).
This makes Spring call that JsonDeserializer (through Jackson, not sure how they work in a lower level) only for the field you want deserialized with your deserializer.

XmlElement ignored by Jackson during serialization

i'm using Jersey to build a REST service and as Json Processor i set Jackson in my application.
#javax.ws.rs.ApplicationPath("/")
public class MyApplication extends ResourceConfig {
public MyApplication() {
packages("controller");
register(JacksonFeature.class);
}
I implement a ContextResolver for Jacksons ObjectMapper (as it's suggested in this post Configure Jersey/Jackson to NOT use #XmlElement field annotation for JSON field naming) which creates an ObjectMapper that doesn't fail on unknown properties during deserialization:
#Provider
public class MyJsonObjectMapperProvider implements ContextResolver<ObjectMapper> {
#Override
public ObjectMapper getContext(Class<?> type)
{
System.out.println("mapper!!!");
ObjectMapper result = new ObjectMapper();
result.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return result;
}
}
and then i register this class in my application inserting register(MyJsonObjectMapperProvider.class) in the class MyApplication shown above. I obtain what i want, in sense that if there are unknown properties in the json the object mapper doesn't fail.
My problem is another; i have this class that i use to map a specified Json, in order to deserialize it and subsequently serialize it:
public class Version {
private String status;
private String updated;
private String id;
private List<Link> links;
#XmlElement(name = "media-types")
private List<MediaTypes> media_types;
//constructor + getter and setter
}
The problem is about the element media_types and the use of the annotation #XmlElement. Before i insert the ContextResolver to personalize ObjectMapper all works fine, in fact after serialization i obtain a json in which the element/attribute media_types has as name media-types; on the contrary with ContextResolver this element doesn't change it's name and has media_types. I think that, during serialization, the annotation XmlElement doesn't work, but i'm not sure that this is the correct reason.
Another attempt i try to do is to put #JsonProperty("media-types") annotation instead of #XmlElement annotation but with no result; in fact with this annotation i obtain also a Processing Exception.
The last attempt (in addition to what has been suggested by the previous post) was that of insert these lines of code in the ContextResolver:
AnnotationIntrospector intr = new AnnotationIntrospector.Pair(new JaxbAnnotationIntrospector(),new JacksonAnnotationIntrospector());
// usually we use same introspector(s) for both serialization and deserialization:
result.getDeserializationConfig().withAnnotationIntrospector(intr);
result.getSerializationConfig().withAnnotationIntrospector(intr);
in order to use both JaxbAnnotation and JacksonAnnotation but the name of the field in question remain media_types.
I hope i was clear in explain my problem and thanks you in advance for your help!

Jackson JSON library: how to instantiate a class with abstract fields that can't access its concrete representation?

This is the same questions than :
Jackson JSON library: how to instantiate a class that contains abstract fields
Nevertheless its solution is not possible since my abstract class is in another project than the concrete one.
Is there a way then ?
EDIT
My architecture is as follows:
public class UserDTO {
...
private LanguageDTO lang;
}
I send that object user :
restTemplate.postForObject(this.getHttpCore().trim() + "admin/user/save/1/" + idUser, userEntity, UserDTO.class);
Then I am supposed to receive it in the function :
#RequestMapping(value = "/save/{admin}/{idUser}", method = RequestMethod.POST)
public String saveUserById(#RequestBody final UserEntity user, #PathVariable Integer idUser, #PathVariable boolean admin)
with UserEntity defined as :
public class UserEntity extends AbstractUserEntity {
...
}
public abstract class AbstractUserEntity {
...
private AbstractLanguageEntity lang;
}
I would like to know how I can specify that lang should be instantiate as LanguageEntity whereas abstract classes are in another project.
This could work assuming you can configure how the object get serialized. See the example here. Look under "1.1. Global default typing" to set the defaults to include extra information in your JSON string, basically the concrete Java type that must be used when deserializing.
Since it seems you need to do this for your Spring servlet, you would have to pass a Spring message converter as mentioned here
Then inside your custom objectMapper, you can do the necessary configuration:
public class JSONMapper extends ObjectMapper {
public JSONMapper() {
this.enableDefaultTyping();
}
}
You could probably also make it work with Mix-ins, which allow you to add annotations to classes already defined. You can see and example here. This will also need to be configured inside the objectMapper.
If you need the same functionality on your client side (REST template), you can pass the object mapper as shown here.
The easiest way to solve that issue is to add getters et setters in UserEntity but specifying a concrete class :
public LanguageEntity getLang() {
return (LanguageEntity) lang;
}
public void setLang(LanguageEntity language){
this.lang = language
}
If all that you want to achieve is to note that LanguageEntity is the implementation of AbstractLanguageEntity, you can register this mapping via module:
SimpleModule myModule = new SimpleModule())
.addAbstractTypeMapping(AbstractLanguageEntity.class,
LanguageEntity.class);
ObjectMapper mapper = new ObjectMapper()
.registerMdoule(myModule);

Categories