I am using Jackson to deserialize JSON of this form:
{
"foo" : { "bar" : "baz" }
}
The jackson code might look like:
#JsonCreator
public class MyProxy {
#JsonProperty("foo") final FooProxy foo;
}
public class FooProxy {
#JsonProperty("bar") final String bar;
}
Imagine a consumer of this API creates invalid JSON like this:
{
"foo" : { "bar" : 1 }
}
However, in this case, I receive a MismatchedInputException and the error looks like this:
Cannot construct instance of MyProxy (although at least one Creator
exists): no int/Int-argument constructor/factory method to deserialize
from Number value (1)
When I inspect the MismatchedInputException, and I call ex.getPathReference() I get:
FooProxy["bar"]->java.lang.Object[0]
I would like to be able to return the path to the broken value to the user without any reference to the underlying Java classes.
"foo.bar must be an Object."
How can I return an error message with the JSON path, and remove any reference to the Java implementation?
Something like this should do it:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
public class DeserializationErrorTest {
#Test
void testDeserializeWrongInput() throws IOException {
final ObjectMapper mapper = new ObjectMapper();
try {
mapper.readValue("{\"foo\" : { \"bar\" : \"not-int\" }}", MyProxy.class);
} catch (MismatchedInputException e) {
throw remapMismatchedInputException(e, RuntimeException.class);
}
}
private <T extends Exception> T remapMismatchedInputException(final MismatchedInputException e, Class<T> exClass) {
try {
final String fieldName =
e.getPath().stream().map(JsonMappingException.Reference::getFieldName).collect(Collectors.joining("."));
return exClass.getConstructor(String.class).newInstance(fieldName + " must be of type " + e.getTargetType().getSimpleName());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException pE) {
throw new IllegalArgumentException("Cannot instantiate exception class " + exClass.getSimpleName());
}
}
static class MyProxy {
#JsonProperty("foo") final FooProxy foo;
#JsonCreator
public MyProxy(#JsonProperty("foo") final FooProxy pFoo) {foo = pFoo;}
}
static class FooProxy {
#JsonProperty("bar") final Integer bar;
#JsonCreator
public FooProxy(#JsonProperty("bar") final Integer pBar) {bar = pBar;}
}
}
And will result in:
java.lang.RuntimeException: foo.bar must be of type Integer
Related
Let us start with sharing working and pastable code (requires google gson package):
package mypackage;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
public final class ALL {
static final Gson GSON = new GsonBuilder().registerTypeAdapterFactory(new Factory()).create();
/////////////////////////////////////////////////////////////////////
#Target( { METHOD, FIELD, ANNOTATION_TYPE, TYPE })
#Retention(RUNTIME)
public #interface Serialize {}
/////////////////////////////////////////////////////////////////////
public static void main(String[] args) {
Test test = new Test();
String json = GSON.toJson(test);
System.out.println(json);
}
/////////////////////////////////////////////////////////////////////
public static final class Test {
#Serialize
String abc = "def";
}
/////////////////////////////////////////////////////////////////////
public static final class Factory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Serialize annotation = type.getRawType().getAnnotation(Serialize.class);
boolean annotationPresent = type.getRawType().isAnnotationPresent(Serialize.class);
Annotation[] annotations = type.getRawType().getAnnotations();
if (annotationPresent) {
System.out.println("11111111111111");
}
if (annotation != null) {
return new Adapter<>();
}
return gson.getDelegateAdapter(this, type);
}
}
/////////////////////////////////////////////////////////////////////
public static final class Adapter<T> extends TypeAdapter<T> {
private static final java.util.Base64.Encoder ENCODER = java.util.Base64.getEncoder();
private static final java.util.Base64.Decoder DECODER = java.util.Base64.getDecoder();
#Override
public T read(JsonReader in) throws IOException {
in.beginObject();
String a = in.nextString();
in.endObject();
try {
return deserialize( DECODER.decode(a) );
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
#Override
public void write(JsonWriter out, T value) throws IOException {
out.value( encode(serialize(value)) );
}
private String encode(byte[] serialize) {
return ENCODER.encodeToString( serialize );
}
private byte[] serialize(T value) throws IOException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream os = new ObjectOutputStream(out); ) {
os.writeObject(value);
return out.toByteArray();
}
}
private T deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
try (ByteArrayInputStream in = new ByteArrayInputStream(bytes); ObjectInputStream is = new ObjectInputStream(in); ) {
return (T) is.readObject();
}
}
}
}
If we look at Test class, the goal is to output something else if the annotation #Serialize is present. In this case we output bytes in String. And when we then read this back, we would like to deserialize it.
Other ways of understanding the goal is to think of maybe using an annotation you would like to encrypt a value and you could decrypt it on readback.
This should be possible, no?
I know i can register TypeAdapters based on field type, however, I would like to be able to use annotations to declare intent instead.
No wrapper classes. You can create a custom JsonSerializer but this requires registering.
In the example above, the type.getRawType().getAnnotation(Serialize.class); is always returning null and Annotation[] annotations = type.getRawType().getAnnotations() always empty, so unable to detect using the factory.
Unsure how to detect the annotation dynamically.
Do you know?
How about using #JsonAdapter? You anyway need to know how to do de-/crypting and need to implement tha per type. For string in your case, for example:
public class CryptoAdapter extends TypeAdapter<String> {
#Override
public void write(JsonWriter out, String value) throws IOException {
out.jsonValue(org.apache.commons.lang3.StringUtils.reverse(value));
}
#Override
public String read(JsonReader in) throws IOException {
return org.apache.commons.lang3.StringUtils.reverse(in.nextString());
}
}
Usage:
public class Test {
#JsonAdapter(CryptoAdapter.class)
String abc = "def";
}
The problem is that Gson does not provide (to my knowledge) any direct means to create some own field processor that lets user to read the field/class member annotations.
In other words you need an access to the field during de-/serialization and that seem not to be possible in an easy way.
That is why there is this #JsonAdapter.
If interested to study more clone source code from GitHub and check:
public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory
Unfortunately final. There is a method named createBoundField (which I think is the logic behind recognizing #JsonAdapter for fields) and the path and overriding that logic is not so straightforward.
For classes there seems to be solution quite similar to yours:
public final class JsonAdapterAnnotationTypeAdapterFactory
implements TypeAdapterFactory
Both above mentioned are added to the list of TypeAdapterFactories when a new Gson is created.
I have been facing an issue that implies Reflection, Annotations and Generics in Java. I have a class that creates a new instance of a generic type called B. Then it will search for any Field with the MyCustomAnnotation annotation and sets its value to a determined one.
The class that does this is:
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class MyInstanceCreator<B> {
private final String myValue = "Hello world!";
public B createInstance(Class<B> classType) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
B obj = classType.getConstructor().newInstance();
for(Field f: classType.getDeclaredFields()) {
if(f.isAnnotationPresent(MyCustomAnnotation.class)) {
System.out.println("Is annotated!");
updateField(obj, f);
}
}
return obj;
}
private void updateField(B instance, Field field) throws IllegalAccessException {
field.setAccessible(true);
field.set(myValue, instance);
field.setAccessible(false);
}
}
The annotation class:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
#Retention(RetentionPolicy.RUNTIME)
public #interface MyCustomAnnotation {}
The custom type has an annotated Field of type String:
public class MyCustomType {
#MyCustomAnnotation
private String value;
public String getValue() {
return value;
}
}
Finally my main class is:
public class MyClass {
public static void main(String args[]) {
try {
MyInstanceCreator<MyCustomType> iCreator = new MyInstanceCreator<>();
MyCustomType myObj = iCreator.createInstance(MyCustomType.class);
System.out.println(myObj.getValue());
} catch(Exception e) {
e.printStackTrace();
}
}
}
The output of the program is:
Is annotated!
java.lang.IllegalArgumentException: Can not set java.lang.String field MyCustomType.value to java.lang.String
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167)
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:171)
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.ensureObj(UnsafeFieldAccessorImpl.java:58)
at java.base/jdk.internal.reflect.UnsafeObjectFieldAccessorImpl.set(UnsafeObjectFieldAccessorImpl.java:75)
at java.base/java.lang.reflect.Field.set(Field.java:780)
at MyInstanceCreator.updateField(MyInstanceCreator.java:21)
at MyInstanceCreator.createInstance(MyInstanceCreator.java:13)
at MyClass.main(MyClass.java:5)
It does not make any sense to me why reflection cannot assign a java.lang.String value to a java.lang.String field as the IllegalArgumentException message says. I must be missing something but I can't seem to figure it out.
Any help is appreciated!
Here's your problem…
...
field.set(myValue, instance);
...
Here's your fix…
...
field.set(instance, myValue);
...
Here are the docs…
public void set(Object obj, Object value)…
...
Parameters:
obj - the object whose field should be modified
value - the new value for the field of obj being modified
…
I'm writing a network class and want to be able to parse different responses to different classes (there's still one-to-one relationship but I want to have a single parseResponse() that will deal with all responses from different endpoints, and endpoint.className has the expected classType that I should map to):
private Class<?> parseResponse(StringBuilder responseContent, Endpoint endpoint) {
ObjectMapper mapper = new ObjectMapper();
try {
Class<?> object = mapper.readValue(responseContent.toString(), endpoint.className);
// endpoint.className has Class<?> type
if (object instanceof endpoint.className) {
}
} catch (IOException e) {
// handle errors
}
}
But there's an error if I write if (object instanceof endpoint.className)
Update: probably the better option is to add parse() method to Endpoint class:
public Class<?> parseResponse(String responseContent) {
// this.className has Class<?> type (e.g., Foo.class).
}
public enum Endpoint {
FOO (Foo.class),
BAR (Bar.class);
private Class<?> classType;
}
But there're still the same type errors.
You should separate JSON deserialisation from other parts of your app. You can not implement one method for all responses but you probably have a limited number of responses and you can declare some simple methods for each class. Generally, you could have only one method with declaration like below:
public <T> T deserialise(String payload, Class<T> expectedClass) {
Objects.requireNonNull(payload);
Objects.requireNonNull(expectedClass);
try {
return mapper.readValue(payload, expectedClass);
} catch (IOException e) {
throw new IllegalStateException("JSON is not valid!", e);
}
}
And now, you can deserialise all payloads you want. You need to provide JSON payload and POJO class you want to receive back.
Simple working solution which shows that concept:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.io.IOException;
import java.util.Objects;
public class JsonMapper {
private final ObjectMapper mapper = new ObjectMapper();
public JsonMapper() {
// configure mapper instance if required
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
// etc...
}
public String serialise(Object value) {
try {
return mapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
throw new IllegalStateException("Could not generate JSON!", e);
}
}
public <T> T deserialise(String payload, Class<T> expectedClass) {
Objects.requireNonNull(payload);
Objects.requireNonNull(expectedClass);
try {
return mapper.readValue(payload, expectedClass);
} catch (IOException e) {
throw new IllegalStateException("JSON is not valid!", e);
}
}
public Foo parseResponseFoo(String payload) {
return deserialise(payload, Foo.class);
}
public Bar parseResponseBar(String payload) {
return deserialise(payload, Bar.class);
}
public static void main(String[] args) {
JsonMapper jsonMapper = new JsonMapper();
String bar = "{\"bar\" : 2}";
System.out.println(jsonMapper.parseResponseBar(bar));
String foo = "{\"foo\" : 1}";
System.out.println(jsonMapper.parseResponseFoo(foo));
System.out.println("General method:");
System.out.println(jsonMapper.deserialise(foo, Foo.class));
System.out.println(jsonMapper.deserialise(bar, Bar.class));
}
}
class Foo {
public int foo;
#Override
public String toString() {
return "Foo{" +
"foo=" + foo +
'}';
}
}
class Bar {
public int bar;
#Override
public String toString() {
return "Bar{" +
"bar=" + bar +
'}';
}
}
See also:
Deserializing or serializing any type of object using Jackson ObjectMapper and handling exceptions
What are Reified Generics? How do they solve Type Erasure problems and why can't they be added without major changes?
How to use jackson to deserialize to Kotlin collections
I have a class like:
public class MyClass {
private final Map<Property, Object> properties;
}
where Property is an enum.
Let's say that properties contains 2 elements, one whose value is a Double and one whose value is a class instance having only one attribute called ownerName. When I serialise this class I get the following string:
{"properties":{"NAME":{"ownerName":"MyBucket"},"DIVISOR":33.0}}
The problem is that when I tried to obtain a MyClass instance from the string above, the value for NAME property will be a Map instead of an instance of the class having ownerName attribute. I tried to write a custom serializer/deserializer but I was not able to do that only for NAME property. Any ideas?
You need to write custom deserialiser for the whole Map. Custom deserialiser could look like below:
class PropertyJsonDeserializer implements JsonDeserializer<Map<Property, Object>>, JsonSerializer<Map<Property, Object>> {
#Override
public Map<Property, Object> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!json.isJsonObject()) {
return Collections.emptyMap();
}
JsonObject root = json.getAsJsonObject();
Map<Property, Object> result = new LinkedHashMap<>();
root.entrySet().forEach(entry -> {
Property property = Property.valueOf(entry.getKey());
switch (property) {
case DIVISOR:
result.put(property, entry.getValue().getAsDouble());
break;
case NAME:
Object owner = context.deserialize(entry.getValue(), Owner.class);
result.put(property, owner);
}
});
return result;
}
#Override
public JsonElement serialize(Map<Property, Object> src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src, Map.class);
}
}
Example usage:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.JsonAdapter;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class GsonApp {
public static void main(String[] args) throws Exception {
Map<Property, Object> properties = new EnumMap<>(Property.class);
properties.put(Property.DIVISOR, new BigDecimal("33.0"));
properties.put(Property.NAME, new Owner());
MyClass myClass = new MyClass(properties);
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String json = gson.toJson(myClass);
System.out.println(json);
myClass = gson.fromJson(json, MyClass.class);
System.out.println(myClass);
}
}
class MyClass {
#JsonAdapter(PropertyJsonDeserializer.class)
private final Map<Property, Object> properties;
public MyClass(Map<Property, Object> properties) {
this.properties = properties;
}
// getters, setters, toString
}
class Owner {
private String ownerName = "MyBucket";
// getters, setters, toString
}
enum Property {
NAME, DIVISOR
}
Above code prints:
{
"properties": {
"NAME": {
"ownerName": "MyBucket"
},
"DIVISOR": 33.0
}
}
MyClass{properties={NAME=Owner{ownerName='MyBucket'}, DIVISOR=33.0}}
I need to change (or remove whole) annotation value on runtime from one class. I got the exapmles from SO but this solution works only for class annotations, not for the field annotations. Any idea how to get this done? Thre reason for this is to make minor change in DB model definition classes to not use enum fields as inmemory db's not having this data type like MySQL does.
Here is working(partially) solution found on SO:
package annotations;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.Map;
public class AnnotationModification2 {
public static void main(String[] args) throws Exception {
Something annotation = (Something) Foobar.class.getAnnotations()[0];
System.out.println("oldAnnotation = " + Foobar.class.getAnnotation(Something.class).someProperty());
changeAnnotationValue(annotation, "someProperty", "another value");
System.out.println("modifiedAnnotation = " + Foobar.class.getAnnotation(Something.class).someProperty());
annotation = (Something) Foobar.class.getDeclaredField("name").getAnnotations()[0];
System.out.println("oldAnnotation = " + annotation.someProperty());
changeAnnotationValue(annotation, "someProperty", "another value");
System.out.println("modifiedAnnotation = " + annotation.someProperty());
System.out.println(Foobar.class.getDeclaredField("name").getAnnotation(Something.class).someProperty());
}
/**
* Changes the annotation value for the given key of the given annotation to newValue and returns
* the previous value.
*/
#SuppressWarnings("unchecked")
public static Object changeAnnotationValue(Annotation annotation, String key, Object newValue){
Object handler = Proxy.getInvocationHandler(annotation);
Field f;
try {
f = handler.getClass().getDeclaredField("memberValues");
} catch (NoSuchFieldException | SecurityException e) {
throw new IllegalStateException(e);
}
f.setAccessible(true);
Map<String, Object> memberValues;
try {
memberValues = (Map<String, Object>) f.get(handler);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
Object oldValue = memberValues.get(key);
if (oldValue == null || oldValue.getClass() != newValue.getClass()) {
throw new IllegalArgumentException();
}
memberValues.put(key,newValue);
return oldValue;
}
#Something(someProperty = "some value")
public static class Foobar {
#Something(someProperty = "Old field value!")
private String name;
}
#Retention(RetentionPolicy.RUNTIME)
#interface Something {
String someProperty();
}
}
Try to use "getField" method:
Something annotation = (Something) Foobar.class.getField("name").getAnnotations()[0];