Why has gson does not allow serialization of java.lang.Class? - java

If you try to serialize an object that has a field of type java.lang.Class, serializing it will lead to java.lang.UnsupportedOperationException: Attempted to serialize java.lang.Class: <some_class> Forgot to register a type adapter
Below is the code snippet from com.google.gson.internal.bind.TypeAdapters.java
public final class TypeAdapters {
.
.
.
public static final TypeAdapter<Class> CLASS = new TypeAdapter<Class>() {
#Override
public void write(JsonWriter out, Class value) throws IOException {
if (value == null) {
out.nullValue();
} else {
throw new UnsupportedOperationException("Attempted to serialize java.lang.Class: "
+ value.getName() + ". Forgot to register a type adapter?");
}
}
.
.
.
}
Was this coded in gson just to remind people if they "Forgot to register a type adapter"?
As I see it, Class type object could have easily been serialized and deserialized using the following statements:
Serialize : clazz.getName()
Deserialize : Class.forName(className)
What could the reason behind the current implementation be? Where am I wrong in this?

as answered by #Programmer Bruce Gson not parsing Class variable -
In a comment in issue 340, a Gson project manager explains:
Serializing types is actually somewhat of a security problem, so we
don't want to support it by default. A malicious .json file could
cause your application to load classes that it wouldn't otherwise;
depending on your class path loading certain classes could DoS your
application.
But it's quite straightforward to write a type adapter to support this
in your own app.
Of course, since serialization is not the same as
deserialization, I don't understand how this is an explanation for the
disabled serialization, unless the unmentioned notion is to in a sense
"balance" the default behaviors of serialization with deserialization.

Related

Jackson serialization failing when upgrading from 2.10 (InvalidDefinitionException: Type id handling not implemented for type java.lang.Object)

I'm upgrading from Jackson 2.10 to 2.12, and suddenly this simple test (which was working fine before) is now failing:
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(DefaultTyping.NON_FINAL);
mapper.valueToTree(new org.joda.time.IllegalFieldValueException("testName", "testValue")); // causes error
java.lang.IllegalArgumentException: Type id handling not implemented for type java.lang.Object (by serializer of type com.fasterxml.jackson.databind.ser.impl.UnsupportedTypeSerializer)
at com.fasterxml.jackson.databind.ObjectMapper.valueToTree(ObjectMapper.java:3312)
at com.amazon.ets.util.exception.ExceptionSerializationTest.shouldSerializeException(ExceptionSerializationTest.java:77)
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Type id handling not implemented for type java.lang.Object (by serializer of type com.fasterxml.jackson.databind.ser.impl.UnsupportedTypeSerializer)
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77)
at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1276)
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400)
at com.fasterxml.jackson.databind.JsonSerializer.serializeWithType(JsonSerializer.java:160)
at com.fasterxml.jackson.databind.ser.impl.TypeWrappedSerializer.serialize(TypeWrappedSerializer.java:32)
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319)
at com.fasterxml.jackson.databind.ObjectMapper.writeValue(ObjectMapper.java:3126)
at com.fasterxml.jackson.databind.ObjectMapper.valueToTree(ObjectMapper.java:3307)
... 24 more
I've gleaned from other similar posts like this one and this one that Jackson can struggle deserializing polymorphic types, but this is erroring on serialization not deserialization. Additionally, when I just try creating my own Exception subclass and try serializing it, it works just fine. I'm trying to use this as a general purpose serializer, so I don't want to have to manually add custom serializers for every object type -- I don't even know why IllegalFieldValueException in particular seems to be the only class that fails to serialize. So I have two main questions:
Why is this suddenly failing when I upgrade from Jackson 2.10 to a later version? I didn't change anything else! Is there a configuration option I can use to have it replicate the earlier version's behavior?
Why is IllegalFieldValueException the only type that seems to be failing to serialize? When I try serializing other exception subclasses or polymorphic types I don't see this error. What's so special about this particular class? (And are there any other classes that might cause the same behavior?)
The short answer is that Jackson basically broke this behavior with this commit: https://github.com/FasterXML/jackson-databind/commit/85c9c8544f0c4f01e88241acc1573746df4f755d
Ironically, there's actually a comment from one of the developers here (tatu) who asks whether they should add an override option allow force-POJO serialization: https://github.com/FasterXML/jackson-databind/blob/2.14/src/main/java/com/fasterxml/jackson/databind/ser/BeanSerializerFactory.java#L891
Unfortunately, this comment from tatu was apparently disregarded, because there's no way to override or disable this check. Even in the latest version of Jackson, it still has this same bad behavior.
The good news is that, as you can see in the implementation of the checkUnsupportedType() method, it only throws this error when attempting to serializing classes under the java.time or org.joda.time packages. That means you don't need to worry about getting this exception thrown when trying to serialize anything else. The bad news is that even if you add the JodaModule to your Jackson mapper, the JodaModule doesn't actually include exception types, so you'll still get this same error.
In the long term, the ideal fix would be for Jackson to add a configurable serialization option to force POJO serialization for time-related types, and/or for the JodaModule to be updated to include the exception types. But for now, you can fix this behavior by creating a subclass of the BeanSerializerFactory:
public class CustomBeanSerializerFactory extends BeanSerializerFactory {
public CustomBeanSerializerFactory(SerializerFactoryConfig config) {
super(config);
}
#Override
protected JsonSerializer<?> _findUnsupportedTypeSerializer(SerializerProvider ctxt, JavaType type, BeanDescription beanDesc) throws JsonMappingException {
return null;
}
#Override
public SerializerFactory withConfig(SerializerFactoryConfig config) {
if (_factoryConfig == config) return this;
return new CustomBeanSerializerFactory(config);
}
}
Then set your ObjectMapper to use this factory instead:
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializerFactory(new CustomBeanSerializerFactory(null));
mapper.enableDefaultTyping(DefaultTyping.NON_FINAL);
mapper.valueToTree(new org.joda.time.IllegalFieldValueException("testName", "testValue")); // no error anymore - yay!

unexpected behavior from custom ObjectInputStream when reading object with LocalDate field

i'm using the example from this repo https://github.com/kojenov/serial/tree/master/3-4.%20upload, which is presenting a method for specifying a way to protect form unsafe deserialization in Java by defining a custom ObjectInputStream and overriding a protected method resolveClass in which we have to specify which classes are allowed for deserialisation.
My problem is I added a LocalDate field to the Planet class and when I deserialize a serialized object I get this exception:
invalid class except unsupported class; java.time.Ser
I searched online and I could not find any other encounter with that problem, so I'm really confused. I tried with instead of the LocalDate to add a LocalDateTime, the same error happens again. As far as I found that class java.time.Ser is a protected class somewhere in the hierarchy of the classes in that package.
The class LocalDate is serializable, so this should not happen. I know for sure that the problem is in the LocalDate, because if I make that field transient code works as intended. Am I missing something or it's just a bug of Java Object Serialization?
By the way, the examples are originally from a talk given by Alexei Kojenov, his site is kojenov.com, but i couldn't find his email to ask him personally.
Serialization is recursive progress, which means when you're serializing a complex object, firstly you need to serialize all its properties. The same thing happens with deserialization.
Planet object contains fields of type int, double and java.lang.String which are primitives and don't need special (de)serialization. LocalDate or LocalDateTime aren't primitives and they're serialized and then deserialized with SafeObjectInputStream.
Serialization hack
As it said in java.io.Serializable documentation, objects can modify their serialization behaviour and even delegate serialization to another class by defining method writeReplace.
JavaDoc cite:
Serializable classes that need to designate an alternative object to be used when writing an object to the stream should implement this special method with the exact signature:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
This writeReplace method is invoked by serialization if the method exists and it would be accessible from a method defined within the class of the object being serialized. Thus, the method can have private, protected and package-private access. Subclass access to this method follows java accessibility rules.
Both LocalDate and LocalDateTime utilizes this possibility and define writeReplace method.
As an example, java.time.LocalDate's implementation:
private Object writeReplace() {
return new Ser(Ser.LOCAL_DATE_TYPE, this);
}
java.time.Ser is a package-private final class that is used as a delegate for java.time.* objects.
Hereby, when you're serializing java.time.LocalDate or java.time.LocalDateTime, actually java.time.Ser being serialized.
Custom deserializer
Previously we found out that java.time.LocalDate was serialized as java.time.Ser. Now, let's try to deserialize it with SafeObjectInputStream.
Before deserialization, resolveClass method is called:
#Override
protected Class<?> resolveClass(ObjectStreamClass input)
throws IOException, ClassNotFoundException
{
if (!input.getName().equals(Planet.class.getName())) {
throw new InvalidClassException("Unsupported class", input.getName());
}
return super.resolveClass(input);
}
It checks for class name to be equal to Planet.class.getName(), but java.time.Ser is not, that's why you're getting an exception.
Solution
To resolve this issue, you need to add java.time.Ser to the list of trusted classes. I would suggest modifying your SafeObjectInputStream next way:
public class SafeObjectInputStream extends ObjectInputStream {
private final List<String> supportedClasses = List.of(Planet.class.getName(), "java.time.Ser");
public SafeObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}
#Override
protected Class<?> resolveClass(ObjectStreamClass input)
throws IOException, ClassNotFoundException
{
if (!supportedClasses.contains(input.getName())) {
throw new InvalidClassException("Unsupported class ", input.getName());
}
return super.resolveClass(input);
}
}
NOTE: List.of was introduced in Java 9. If your Java version is less than 9, you can replace it with Arrays.asList.

Jackson Deserializer - inject key:value pairs if not present in serialized data

I save the payload of events as JSON string into a database.
To only have one version of the events in code while still being able to read old events I want to "upcast" the serialized data before deserialization.
I thought about something like this:
public Object deserialize(String data, Class<?> clazz) throws IOException {
data = upcaster.upcast(data, clazz);
return objectMapper.readValue(data, clazz);
}
But this means I transform the string into some JSON object twice. Once for upcasting the data and once inside the standard jackson mapper.
Is there a way to customize jackson between building the json-object and building my event object?
Something like:
#override
updateData(clazz, jsonData) {
if(clazz.equals(SpecificEvent.class)) {
if(!jsonData.containsKey("addedInfo")) {
jsonData.put("addedInfo", "foo");
}
}
}
Alternatively, I could add my own deserializer that changes the given JSON data before calling the standard deserializer I guess?
What's the normal way of doing something like this with jackson?
One option is custom deserialization with a dedicated deserializer class (per your own class). You would then have to maintain the deserializer class to make sure that it provides any missing data.
It should work for a handful of classes, if you have a lot then there might be better ways.

how to use method canDeserialize ObjectMapper class from jackson API?

I want to use method canDeserialize, because at moment deserialization i want to get type class for apply at custom deserialization, as about next example :
public T deserialize(byte[] bytes) throws SerializationException {
bolean isAccount = this.objectMapper.canDeserialize(??????).
T t = null;
if(isAccount)
t = (T)this.objectMapper.readValue(bytes,Account.class);
else
t = (T) this.objectMapper.readValue(bytes, 0, bytes.length, new TypeReference<Object>(){});
return t;
}
In this case Account class have annotation #JsonDeserialize for a custom deserialization .
To directly answer your question, this is how you use the canDeserialize method:
final ObjectMapper mapper = new ObjectMapper();
mapper.canDeserialize(mapper.constructType(Bean.class));
Where Bean is the name of your Java class to be checked.
But wait, you are trying to solve the wrong problem. You are struggling with the logic for your method because it has not been designed properly. You are really asking too much of the Java runtime (and Jackson library), by trying to make them infer all the required information about the type to be instantiated (based on the parameterized return). To solve this you should include the class representing the type to be deserialized as a parameter to the method, greatly simplifying the logic:
public <T> T deserialize(byte[] bytes, Class<T> clazz) throws IOException,
JsonProcessingException {
return new ObjectMapper().readValue(bytes, clazz);
}
At this point you have probably realized that the method above provides no additional functionality over just calling ObjectMapper.readValue directly, so ... just do that! No need to define custom methods, just use ObjectMapper and you are good to go. Keep in mind that you do not need to do anything explicit to trigger custom deserialization of classes. The Jackson runtime automatically detects when a class has a custom deserializer and invokes it.

Jackson JSON library: how to instantiate a class that contains abstract fields

I want to convert a JSON string into java object, but the class of this object contains abstract fields, which Jackson can't instantiate, and doesn't produce the object. What is the easiest way to tell it about some default implementation of an abstract class, like
setDefault(AbstractAnimal.class, Cat.class);
or to decide about the implementation class based on JSON attribute name, eg. for JSON object:
{
...
cat: {...}
...
}
i would just wite:
setImpl("cat", Cat.class);
I know it's possible in Jackson to embed class information inside JSON, but I don't want to complicate the JSON format I use. I want to decide what class to use just by setting default implementation class, or by the attribute name ('cat') - like in XStream library, where you write:
xStream.alias("cat", Cat.class);
Is there a way to do so, especially in one line, or does it require some more code?
There are multiple ways; before version 1.8, simplest way is probably to do:
#JsonDeserialize(as=Cat.class)
public abstract class AbstractAnimal { ... }
as to deciding based on attribute, that is best done using #JsonTypeInfo, which does automatic embeddeding (when writing) and use of type information.
There are multiple kinds of type info (class name, logical type name), as well as inclusion mechanisms (as-included-property, as-wrapper-array, as-wrapper-object). This page: https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization explains some of the concepts.
A full fledged answer with a very clear example can be found here: https://stackoverflow.com/a/30386694/584947
Jackson refers to this as Polymorphic Deserialization.
It definitely helped me with my issue. I had an abstract class that I was saving in a database and needed to unmarshal it to a concrete instance of a class (understandably).
It will show you how to properly annotate the parent abstract class and how to teach jackson how to pick among the available sub-class candidates at run-time when unmarshaling.
If you want to pollute neither your JSON with extra fields nor your classes with annotation, you can write a very simple module and deserializer that uses the default subclass you want. It is more than one line due to some boilerplate code, but it is still relatively simple.
class AnimalDeserializer extends StdDeserializer<Animal> {
public AnimalDeserializer() {
super(Animal.class);
}
public Animal deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
return jsonParser.readValueAs(Cat.class);
}
}
class AnimalModule extends SimpleModule {
{
addDeserializer(Animal.class, new AnimalDeserializer());
}
}
Then register this module for the ObjectMapper and that's it (Zoo is the container class that has an Animal field).
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new AnimalModule());
return objectMapper.readValue(json, Zoo.class);
The problem can be solved with the annotation #JsonDeserialize on the abstract class.
Refers to Jackson Exceptions Problems and Solutions for more info

Categories

Resources