I can find plenty of examples of polymorphic deserialization based on a field within an object:
[
{
"type": "Engine",
"name": "Ford 6.7L",
"cylinders": 8
},
{
"type": "Tires",
"name": "Blizzak LM32",
"season": "winter"
}
]
But I can't seem to easily put together something that'll use object keys to determine type:
{
"Engine": {
"name": "Ford 6.7L",
"cylinders": 8
},
"Tires": {
"name": "Blizzak LM32",
"season": "winter"
}
}
without first parsing the file into a JsonObject and then iterating through that object and re-converting each value back to a string and re-parsing into a class based on the key (and rolling my own method of tracking types per key).
Ideally, I'd like to do something along these lines:
#JsonKey("Engine")
class Engine implements Equipment {
String name;
Integer cylinders;
}
#JsonKey("Tires")
class Tires implements Equipment {
String name;
String season;
}
And be able to parse the file like this:
Map<String, Equipment> car = gson.fromJson(fileContents, new TypeToken<Map<String, Equipment>>(){}.getType();
This seems like a pretty obvious use case to me. What am I missing?
There is nothing good in using object names as key to deserialize polymorphic type. This is leading to having multiple object's with same name being part of parent object (your case). When you would try to deserialize parent JSON object (in future there might be parent object containing attribute's Engine and Tires) you could end up with multiple JSON object's representing this attribute with same name (repeating type name) leading to parser exception.
Deserialization based on type attribute inside JSON object is common and convenient way. You could implement code to work as you expect but it would be not error prone in all cases and therefore JSON parser implementation's expect, in this case, to deserialize polymorphic type by nested type attribute which is error prone and clean way to do so.
Edit:
What you are trying to achieve is also against separation of concern (JSON object key is key itself and also type key at same time) while type attribute separate's type responsibility to one of JSON object's attribute's. That is also following KISS principle (keep it stupid simple) and also many of developer's are used to type attribute's in case of polymorphic deserialization.
All you have to do is to implement a custom Map<String, ...> deserializer that will be triggered for maps defined using a special deserializer that's aware of the mapping rules.
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Inherited
#interface JsonKey {
#Nonnull
String value();
}
final class JsonKeyMapTypeAdapterFactory<V>
implements TypeAdapterFactory {
private final Class<V> superClass;
private final Map<String, Class<? extends V>> subClasses;
private final Supplier<? extends Map<String, V>> createMap;
private JsonKeyMapTypeAdapterFactory(final Class<V> superClass, final Map<String, Class<? extends V>> subClasses,
final Supplier<? extends Map<String, V>> createMap) {
this.superClass = superClass;
this.subClasses = subClasses;
this.createMap = createMap;
}
static <V> Builder<V> build(final Class<V> superClass) {
return new Builder<>(superClass);
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !Map.class.isAssignableFrom(typeToken.getRawType()) ) {
return null;
}
final Type type = typeToken.getType();
if ( !(type instanceof ParameterizedType) ) {
return null;
}
final ParameterizedType parameterizedType = (ParameterizedType) type;
final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
final Type valueType = actualTypeArguments[1];
if ( !(valueType instanceof Class) ) {
return null;
}
final Class<?> valueClass = (Class<?>) valueType;
if ( !superClass.isAssignableFrom(valueClass) ) {
return null;
}
final Type keyType = actualTypeArguments[0];
if ( !(keyType instanceof Class) || keyType != String.class ) {
throw new IllegalArgumentException(typeToken + " must represent a string-keyed map");
}
final Function<? super String, ? extends TypeAdapter<? extends V>> resolveTypeAdapter = subClasses.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> gson.getDelegateAdapter(this, TypeToken.get(e.getValue()))))
::get;
#SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) JsonKeyMapTypeAdapter.create(resolveTypeAdapter, createMap);
return castTypeAdapter;
}
static final class Builder<V> {
private final Class<V> superClass;
private final ImmutableMap.Builder<String, Class<? extends V>> subClasses = new ImmutableMap.Builder<>();
private Supplier<? extends Map<String, V>> createMap = LinkedHashMap::new;
private Builder(final Class<V> superClass) {
this.superClass = superClass;
}
Builder<V> register(final Class<? extends V> subClass) {
#Nullable
final JsonKey jsonKey = subClass.getAnnotation(JsonKey.class);
if ( jsonKey == null ) {
throw new IllegalArgumentException(subClass + " must be annotated with " + JsonKey.class);
}
return register(jsonKey.value(), subClass);
}
Builder<V> register(final String key, final Class<? extends V> subClass) {
if ( !superClass.isAssignableFrom(subClass) ) {
throw new IllegalArgumentException(subClass + " must be a subclass of " + superClass);
}
subClasses.put(key, subClass);
return this;
}
Builder<V> createMapWith(final Supplier<? extends Map<String, V>> createMap) {
this.createMap = createMap;
return this;
}
TypeAdapterFactory create() {
return new JsonKeyMapTypeAdapterFactory<>(superClass, subClasses.build(), createMap);
}
}
private static final class JsonKeyMapTypeAdapter<V>
extends TypeAdapter<Map<String, V>> {
private final Function<? super String, ? extends TypeAdapter<? extends V>> resolveTypeAdapter;
private final Supplier<? extends Map<String, V>> createMap;
private JsonKeyMapTypeAdapter(final Function<? super String, ? extends TypeAdapter<? extends V>> resolveTypeAdapter,
final Supplier<? extends Map<String, V>> createMap) {
this.resolveTypeAdapter = resolveTypeAdapter;
this.createMap = createMap;
}
private static <V> TypeAdapter<Map<String, V>> create(final Function<? super String, ? extends TypeAdapter<? extends V>> resolveTypeAdapter,
final Supplier<? extends Map<String, V>> createMap) {
return new JsonKeyMapTypeAdapter<>(resolveTypeAdapter, createMap)
.nullSafe();
}
#Override
public void write(final JsonWriter out, final Map<String, V> value) {
throw new UnsupportedOperationException();
}
#Override
public Map<String, V> read(final JsonReader in)
throws IOException {
in.beginObject();
final Map<String, V> map = createMap.get();
while ( in.hasNext() ) {
final String key = in.nextName();
#Nullable
final TypeAdapter<? extends V> typeAdapter = resolveTypeAdapter.apply(key);
if ( typeAdapter == null ) {
throw new JsonParseException("Unknown key " + key + " at " + in.getPath());
}
final V value = typeAdapter.read(in);
#Nullable
final V replaced = map.put(key, value);
if ( replaced != null ) {
throw new JsonParseException(value + " duplicates " + replaced + " using " + key);
}
}
in.endObject();
return map;
}
}
}
private static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapterFactory(JsonKeyMapTypeAdapterFactory.build(Equipment.class)
.register(Engine.class)
.register(Tires.class)
.create()
)
.create();
The Gson object above will deserialize your JSON document to a map that is toString-ed like this (assuming Lombok is used for toString):
{Engine=Engine(name=Ford 6.7L, cylinders=8), Tires=Tires(name=Blizzak LM32, season=winter)}
Related
I have a message in JSON format that I converted to a JSONObject, and I have around 30 mandatory fields that I have to check for whether they're null or not. If one of these mandatory fields are null, I will discard the message, however other fields can be null without needing to discard the message. Is there any efficient way I can do this without going through each and every field and using isNull() ?
Also, the JSON objects are nested, so a simple anyNull() function would not work since it would only return if the object itself is null and not if the variables themselves are null.
I tried using gson to convert the message to a POJO, and created classes for 10 objects
Gson gson = new Gson();
Message message = gson.fromJson(msg, Message.class);
but since many classes are nested (and one of which is an array of objects) using simple null checkers don't work.
Actually speaking your question is not very clear because you're using a word of "message" that refers your particular class, but can also be more generic referring sent/received messages.
So something like for JSON elements in memory:
public static void failOnNullRecursively(final JsonElement jsonElement) {
if ( jsonElement.isJsonNull() ) {
throw new IllegalArgumentException("null!");
}
if ( jsonElement.isJsonPrimitive() ) {
return;
}
if ( jsonElement.isJsonArray() ) {
for ( final JsonElement element : jsonElement.getAsJsonArray() ) {
failOnNullRecursively(element);
}
return;
}
if ( jsonElement.isJsonObject() ) {
for ( final Map.Entry<String, JsonElement> e : jsonElement.getAsJsonObject().entrySet() ) {
failOnNullRecursively(e.getValue());
}
return;
}
throw new AssertionError(jsonElement);
}
or JSON documents in streams:
public final class FailOnNullJsonReader
extends JsonReader {
private FailOnNullJsonReader(final Reader reader) {
super(reader);
}
public static JsonReader create(final Reader reader) {
return new FailOnNullJsonReader(reader);
}
#Override
public void nextNull() {
throw new IllegalStateException(String.format("null at %#!", getPath()));
}
}
Both of them will throw on null. But it also seems that you want to validate Message instances:
If one of these mandatory fields are null, I will discard the message, however other fields can be null without needing to discard the message.
So this tells why the above null-checks won't fit your needs. What you're looking for is JSR-303. It won't be that efficient as you might want to want it to be (message instances are deserialized, validation takes time and resources too), but it might be efficient from the coding perspective:
final Set<ConstraintViolation<V>> violations = validator.validate(message);
if ( !violations.isEmpty() ) {
throw new ConstraintViolationException(violations);
}
or even integrate it right into Gson so that it serves middleware:
public final class PostReadTypeAdapterFactory<V>
implements TypeAdapterFactory {
private final Predicate<? super TypeToken<?>> supports;
private final BiConsumer<? super TypeToken<V>, ? super V> onRead;
private PostReadTypeAdapterFactory(final Predicate<? super TypeToken<?>> supports, final BiConsumer<? super TypeToken<V>, ? super V> onRead) {
this.supports = supports;
this.onRead = onRead;
}
public static <V> TypeAdapterFactory create(final Predicate<? super TypeToken<?>> supports, final BiConsumer<? super TypeToken<V>, ? super V> onRead) {
return new PostReadTypeAdapterFactory<>(supports, onRead);
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !supports.test(typeToken) ) {
return null;
}
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value)
throws IOException {
delegate.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
final T readValue = delegate.read(in);
#SuppressWarnings("unchecked")
final V value = (V) readValue;
#SuppressWarnings("unchecked")
final TypeToken<V> valueTypeToken = (TypeToken<V>) typeToken;
onRead.accept(valueTypeToken, value);
return readValue;
}
};
}
}
public final class Jsr303Support {
private Jsr303Support() {
}
public static <V> TypeAdapterFactory createTypeAdapterFactory(final Validator validator) {
return PostReadTypeAdapterFactory.<V>create(
typeToken -> typeToken.getRawType().isAnnotationPresent(Validate.class),
(typeToken, value) -> {
final Set<ConstraintViolation<V>> violations = validator.validate(value);
if ( !violations.isEmpty() ) {
throw new ConstraintViolationException(violations);
}
}
);
}
}
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface Validate {
}
And the test (using Lombok for brevity):
#Validate
#AllArgsConstructor
#EqualsAndHashCode
#ToString
final class Message {
#NotNull
final String foo;
#NotNull
final String bar;
#NotNull
final String baz;
}
public final class Jsr303SupportTest {
private static final Validator validator;
static {
try ( final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory() ) {
validator = validatorFactory.getValidator();
}
}
public static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.registerTypeAdapterFactory(Jsr303Support.createTypeAdapterFactory(validator))
.create();
#Test
public void test() {
Assertions.assertEquals(new Message("1", "2", "3"), gson.fromJson("{\"foo\":\"1\",\"bar\":\"2\",\"baz\":\"3\"}", Message.class));
final ConstraintViolationException ex = Assertions.assertThrows(ConstraintViolationException.class, () -> gson.fromJson("{\"foo\":\"1\",\"bar\":null,\"baz\":\"3\"}", Message.class));
Assertions.assertEquals(1, ex.getConstraintViolations().size());
}
}
And finally, probably the most efficient (in terms of reading JSON stream), but very limited whencompared to JSR-303 (and NOT working in Gson because Gson does not propagate null-checking to downstream (de)serializers), way that could replace #NotNull with a similar "functional" annotation:
public final class NotNullTypeAdapterFactory
implements TypeAdapterFactory {
// note no external access
private NotNullTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final TypeAdapter<T> delegate = gson.getAdapter(typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, #Nullable final T value)
throws IOException {
if ( value == null ) {
throw new IllegalArgumentException(typeToken + " with null");
}
delegate.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
#Nullable
final T value = delegate.read(in);
if ( value == null ) {
throw new IllegalArgumentException(typeToken + " with null at " + in.getPath());
}
return value;
}
};
}
}
#AllArgsConstructor
#EqualsAndHashCode
#ToString
final class Message {
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String foo;
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String bar;
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String baz;
}
public final class NotNullTypeAdapterFactoryTest {
public static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.create();
#Test
public void test() {
Assertions.assertEquals(new Message("1", "2", "3"), gson.fromJson("{\"foo\":\"1\",\"bar\":\"2\",\"baz\":\"3\"}", Message.class));
final IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> gson.fromJson("{\"foo\":\"1\",\"bar\":null,\"baz\":\"3\"}", Message.class));
Assertions.assertEquals("whatever here, the above does not work anyway", ex.getMessage());
}
}
The third, JSR-303, looks like the best for you.
Given a JSON map like this:
{
"category1": [],
"category2": ["item1"],
"category3": ["item1", "item2"]
}
I would like to convert it into a Java ArrayList of Categories (not a Map), like this:
class Category {
final String categoryName;
final ArrayList<String> items;
public Category(String categoryName, ArrayList<String> items) {
this.categoryName = categoryName;
this.items = items;
}
}
ArrayList<Category> data = new ArrayList<>();
So I expect data to look like this:
for (Category c: data) {
System.out.println(c.categoryName + ", " + c.items);
}
/**
Expected data =
category1, empty list [] or null
category2, [item1]
category2, [item1, item2]
**/
I've tried using the Gson library:
app/build.gradle:
dependencies {
...
implementation 'com.google.code.gson:gson:2.8.6'
}
JsonConverterTest.java
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonParser;
import com.google.gson.internal.LinkedTreeMap;
import com.google.gson.reflect.TypeToken;
import org.junit.Test;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class JsonConverterTest {
#Test
public void gsonMapToListTest() {
String json = "{\"category1\": [],\"category2\": [\"item1\"],\"category3\": [\"item1\", \"item2\"]}";
class Category {
final String categoryName;
final ArrayList<String> items;
public Category(String categoryName, ArrayList<String> items) {
this.categoryName = categoryName;
this.items = items;
}
}
ArrayList<Category> expectedData = new ArrayList<>();
expectedData.add(new Category("category1", new ArrayList<String>()));
expectedData.add(new Category("category2", new ArrayList<>(Arrays.asList("item1"))));
expectedData.add(new Category("category2", new ArrayList<>(Arrays.asList("item1", "item2"))));
System.out.println("Expected Data:");
for (Category c: expectedData) {
System.out.println(c.categoryName + ", " + c.items);
}
// This works, but it returns a Map instead of a List
LinkedTreeMap<String, ArrayList<String>> dataMap = new Gson().fromJson(json, LinkedTreeMap.class);
System.out.println("\n data as Map = " + dataMap);
// This causes "IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $"
Type listOfCategoriesType = new TypeToken<ArrayList<Category>>() {}.getType();
ArrayList<Category> data = new Gson().fromJson(json, listOfCategoriesType); // IllegalStateException here
assertThat(data, is(expectedData));
// This causes "java.lang.IllegalStateException: Not a JSON Array: {...}"
JsonArray jsonArray = JsonParser.parseString(json).getAsJsonArray(); // IllegalStateException here
data = new Gson().fromJson(jsonArray, listOfCategoriesType);
assertThat(data, is(expectedData));
}
}
Using the guides https://www.baeldung.com/gson-list and https://futurestud.io/tutorials/gson-mapping-of-maps , I can only convert the JSON map to a Java Map. But I get an IllegalStateException if I try to convert the JSON map to a Java List:
Type listOfCategoriesType = new TypeToken<ArrayList<Category>>() {}.getType();
ArrayList<Category> data = new Gson().fromJson(json, listOfCategoriesType); // IllegalStateException
or
JsonArray jsonArray = JsonParser.parseString(json).getAsJsonArray(); // IllegalStateException
ArrayList<Category> data2 = new Gson().fromJson(jsonArray, listOfCategoriesType);
So what is the correct way in Gson to convert the Json Map to a Java list, as per the unit test gsonMapToListTest() above?
Easiest is to simply parse into Map, then convert Map to List<Category>.
LinkedTreeMap<String, ArrayList<String>> dataMap = new Gson().fromJson(json,
new TypeToken<LinkedTreeMap<String, ArrayList<String>>>() {}.getType());
ArrayList<Category> dataList = new ArrayList<>();
for (Entry<String, ArrayList<String>> entry : dataMap.entrySet())
dataList.add(new Category(entry.getKey(), entry.getValue()));
The alternative is to write your own TypeAdapter, or to use another JSON library.
Not the easiest way, but Gson allows implementing a type adapter like this so you can deserialize the JSON in a result object without intermediate collections.
public final class CombinerTypeAdapterFactory<V, C>
implements TypeAdapterFactory {
// used to create a collection to populate
private final TypeToken<? extends Collection<? super C>> collectionTypeToken;
// represents a type of each map entry value
private final TypeToken<V> valueTypeToken;
// a strategy to map an entry to an element
private final BiFunction<? super String, ? super V, ? extends C> combine;
private CombinerTypeAdapterFactory(final TypeToken<? extends Collection<? super C>> collectionTypeToken, final TypeToken<V> valueTypeToken,
final BiFunction<? super String, ? super V, ? extends C> combine) {
this.collectionTypeToken = collectionTypeToken;
this.valueTypeToken = valueTypeToken;
this.combine = combine;
}
public static <V, C> TypeAdapterFactory create(final TypeToken<List<C>> collectionTypeToken, final TypeToken<V> valueTypeToken,
final BiFunction<? super String, ? super V, ? extends C> combine) {
return new CombinerTypeAdapterFactory<>(collectionTypeToken, valueTypeToken, combine);
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// check if supported
if ( !typeToken.equals(collectionTypeToken) ) {
return null;
}
// grab the value type adapter
final TypeAdapter<V> valueTypeAdapter = gson.getAdapter(valueTypeToken);
// and the type adapter the collection to be populated
final TypeAdapter<? extends Collection<? super C>> collectionTypeAdapter = gson.getDelegateAdapter(this, collectionTypeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value) {
throw new UnsupportedOperationException(); // TODO
}
#Override
public T read(final JsonReader in)
throws IOException {
// assuming all of those are always { e1, e2, e3, ... }
in.beginObject();
// a hack to create an empty collection (it's modifiable right?)
final Collection<? super C> collection = collectionTypeAdapter.fromJson("[]");
// for each entry in the map that's being read
while ( in.hasNext() ) {
// get is name
final String name = in.nextName();
// and its value
final V value = valueTypeAdapter.read(in);
// combine
final C combined = combine.apply(name, value);
// and add to the list
collection.add(combined);
}
in.endObject();
// we know better than javac
#SuppressWarnings("unchecked")
final T result = (T) collection;
return result;
}
}
.nullSafe();
}
}
The following test is green then:
public final class CombinerTypeAdapterFactoryTest {
#AllArgsConstructor
#EqualsAndHashCode
#ToString
private static final class Category {
#Nullable
final String categoryName;
#Nullable
final List<String> items;
}
private static final Gson gson = new GsonBuilder()
.disableInnerClassSerialization()
.disableHtmlEscaping()
.registerTypeAdapterFactory(CombinerTypeAdapterFactory.create(new TypeToken<List<Category>>() {}, new TypeToken<List<String>>() {}, Category::new))
.create();
#Test
public void test()
throws IOException {
try ( final JsonReader jsonReader = new JsonReader(new InputStreamReader(CombinerTypeAdapterFactoryTest.class.getResourceAsStream("input.json"))) ) {
final List<Category> actualCategories = gson.fromJson(jsonReader, new TypeToken<List<Category>>() {}.getType());
Assertions.assertIterableEquals(
ImmutableList.of(
new Category("category1", ImmutableList.of()),
new Category("category2", ImmutableList.of("item1")),
new Category("category3", ImmutableList.of("item1", "item2"))
),
actualCategories
);
}
}
}
I'm trying to figure out how to combine the code generator I use in my project and Jackson so that I could combine them both.
The third-party bean code generator does some things that I would like to improve.
For example, the class below
public class Wrapper {
public String string;
public List<String> array;
}
does not have default values set for both string and array.
Under some circumstances (and mostly due to heavy legacy reasons) I'd like Jackson to deserialize the above bean class with the default values set if they are not provided in the input JSON document.
For example, I'd like {"string": "foo"} to be deserialized to a bean as if the source JSON were {"string":"foo","array":[]} so that it would result in a bean with two non-null fields.
The first idea I came up with is creating a bean instance, then run a "set default fields" preprocessor, and then read the JSON into the constructed and initialized bean.
public final class DefaultsModule
extends SimpleModule {
#Override
public void setupModule(final SetupContext setupContext) {
setupContext.addBeanDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(final DeserializationConfig config, final BeanDescription description,
final JsonDeserializer<?> defaultDeserializer) {
return DefaultFieldsJsonDeserializer.create(description.getType(), description);
}
});
}
private static final class DefaultFieldsJsonDeserializer<T>
extends JsonDeserializer<T> {
// the generated classes set is finite, so won't bother with subclassing
private static final Map<Class<?>, Supplier<?>> NEW_INSTANCES = new ImmutableMap.Builder<Class<?>, Supplier<?>>()
.put(Iterable.class, ArrayList::new)
.put(Collection.class, ArrayList::new)
.put(List.class, ArrayList::new)
.put(ArrayList.class, ArrayList::new)
.put(LinkedList.class, LinkedHashMap::new)
.put(Map.class, LinkedHashMap::new)
.put(HashMap.class, HashMap::new)
.put(LinkedHashMap.class, LinkedHashMap::new)
.put(TreeMap.class, TreeMap::new)
.put(Set.class, LinkedHashSet::new)
.put(HashSet.class, HashSet::new)
.put(LinkedHashSet.class, LinkedHashSet::new)
.put(TreeSet.class, TreeSet::new)
.build();
private final BeanDescription description;
private final Iterable<? extends Map.Entry<Field, ? extends Supplier<?>>> fieldDefaultsChain;
private DefaultFieldsJsonDeserializer(final BeanDescription description,
final Iterable<? extends Map.Entry<Field, ? extends Supplier<?>>> fieldDefaultsChain) {
this.description = description;
this.fieldDefaultsChain = fieldDefaultsChain;
}
private static <T> JsonDeserializer<T> create(final JavaType javaType, final BeanDescription description) {
final Iterable<? extends Map.Entry<Field, ? extends Supplier<?>>> fieldDefaultsChain = Stream.of(javaType.getRawClass().getDeclaredFields())
.filter(field -> NEW_INSTANCES.containsKey(field.getType()))
.peek(field -> field.setAccessible(true))
.map(field -> new AbstractMap.SimpleImmutableEntry<Field, Supplier<Object>>(field, () -> NEW_INSTANCES.get(field.getType()).get()))
.collect(Collectors.toList());
return new DefaultFieldsJsonDeserializer<>(description, fieldDefaultsChain);
}
#Override
#Nullable
public T deserialize(final JsonParser parser, final DeserializationContext context)
throws IOException {
try {
// instantiate the bean
#Nullable
#SuppressWarnings("unchecked")
final T bean = (T) description.instantiateBean(false);
if ( bean == null ) {
return null;
}
// do default values pre-processing
for ( final Map.Entry<Field, ? extends Supplier<?>> e : fieldDefaultsChain ) {
final Field field = e.getKey();
final Object defaultValue = e.getValue().get();
field.set(bean, defaultValue);
}
// since the object is constructed and initialized properly, simply update it
final ObjectReader objectReader = ((ObjectMapper) parser.getCodec())
.readerForUpdating(bean);
return objectReader.readValue(parser);
} catch ( final IllegalAccessException ex ) {
return context.reportBadTypeDefinition(description, ex.getMessage());
}
}
}
}
In short, I'd like the following unit test to pass:
public final class DefaultsModuleTest {
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode
private static final class Generated {
#JsonProperty
private String string;
#JsonProperty
private List<String> array /*not generated but should be initialized in the pre-processor = new ArrayList<>()*/;
}
#Test
public void test()
throws IOException {
final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new DefaultsModule());
final Generated expected = new Generated("foo", Collections.emptyList());
Assertions.assertEquals(expected, objectMapper.readValue("{\"string\":\"foo\"}", Generated.class));
Assertions.assertEquals(expected, objectMapper.readValue("{\"string\":\"foo\",\"array\":null}", Generated.class));
Assertions.assertEquals(expected, objectMapper.readValue("{\"string\":\"foo\",\"array\":[]}", Generated.class));
}
}
Unfortunately, the deserializer above runs in the infinite recursion loop.
So I have multiple questions:
how to implement it properly?
maybe I should go with ValueInstantiator somehow?
what is a generic way to get the delegate JSON deserializer? (Gson allows to obtain delegate type adapters in type adapter factories, Jackson offers the deserializer modifier approach but the JsonDeserializer coming in the modifier causes weird exceptions + not sure if it can update existing objects).
My Jackson databind version is 2.9.10.
I seem to have realized the way it had to be done properly. I didn't notice that I can add value instantiators I mentioned above to the module setup context. Having it configured, I simply don't need to create a custom deserializer since I can supply constructed+initialized values myself.
public final class DefaultsModule
extends SimpleModule {
#Override
public void setupModule(final SetupContext setupContext) {
setupContext.addValueInstantiators((config, description, defaultInstantiator) -> DefaultFieldsInstantiator.isSupported(description.getBeanClass())
? DefaultFieldsInstantiator.create(config, description)
: defaultInstantiator
);
}
private static final class DefaultFieldsInstantiator
extends StdValueInstantiator {
private static final Map<Class<?>, Supplier<?>> NEW_INSTANCES = new ImmutableMap.Builder<Class<?>, Supplier<?>>()
.put(Iterable.class, ArrayList::new)
.put(Collection.class, ArrayList::new)
.put(List.class, ArrayList::new)
.put(ArrayList.class, ArrayList::new)
.put(LinkedList.class, LinkedHashMap::new)
.put(Map.class, LinkedHashMap::new)
.put(HashMap.class, HashMap::new)
.put(LinkedHashMap.class, LinkedHashMap::new)
.put(TreeMap.class, TreeMap::new)
.put(Set.class, LinkedHashSet::new)
.put(HashSet.class, HashSet::new)
.put(LinkedHashSet.class, LinkedHashSet::new)
.put(TreeSet.class, TreeSet::new)
.build();
private final BeanDescription description;
private final Iterable<? extends Map.Entry<Field, ? extends Supplier<?>>> fieldDefaultsChain;
private DefaultFieldsInstantiator(final DeserializationConfig config, final BeanDescription description,
final Iterable<? extends Map.Entry<Field, ? extends Supplier<?>>> fieldDefaultsChain) {
super(config, description.getType());
this.description = description;
this.fieldDefaultsChain = fieldDefaultsChain;
}
private static boolean isSupported(final Class<?> clazz) {
return ...............;
}
private static ValueInstantiator create(final DeserializationConfig config, final BeanDescription description) {
final Iterable<? extends Map.Entry<Field, ? extends Supplier<?>>> fieldDefaultsChain = Stream.of(description.getType().getRawClass().getDeclaredFields())
.filter(field -> NEW_INSTANCES.containsKey(field.getType()))
.peek(field -> field.setAccessible(true))
.map(field -> new AbstractMap.SimpleImmutableEntry<Field, Supplier<Object>>(field, () -> NEW_INSTANCES.get(field.getType()).get()))
.collect(Collectors.toList());
return new DefaultFieldsInstantiator(config, description, fieldDefaultsChain);
}
#Override
public boolean canCreateUsingDefault() {
return true;
}
#Override
#Nullable
public Object createUsingDefault(final DeserializationContext context)
throws JsonMappingException {
try {
#Nullable
final Object bean = description.instantiateBean(false);
if ( bean == null ) {
return null;
}
for ( final Map.Entry<Field, ? extends Supplier<?>> e : fieldDefaultsChain ) {
final Field field = e.getKey();
final Object defaultValue = e.getValue().get();
field.set(bean, defaultValue);
}
return bean;
} catch ( final IllegalAccessException ex ) {
return context.reportBadDefinition(description.getType(), "Cannot set field: " + ex.getMessage());
}
}
}
}
And all the following tests pass:
final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new DefaultsModule());
Assertions.assertNull(objectMapper.readValue("null", Generated.class));
Assertions.assertEquals(new Generated(null, Collections.emptyList()), objectMapper.readValue("{}", Generated.class));
Assertions.assertEquals(new Generated(null, ImmutableList.of("bar")), objectMapper.readValue("{\"array\":[\"bar\"]}", Generated.class));
Assertions.assertEquals(new Generated("foo", Collections.emptyList()), objectMapper.readValue("{\"string\":\"foo\"}", Generated.class));
Assertions.assertEquals(new Generated("foo", null), objectMapper.readValue("{\"string\":\"foo\",\"array\":null}", Generated.class));
Assertions.assertEquals(new Generated("foo", Collections.emptyList()), objectMapper.readValue("{\"string\":\"foo\",\"array\":[]}", Generated.class));
I have a following interface
public interface Splitter<T, V> {
V[] split(T arg);
}
Following is the factory method implementation which I am using to get Splitter Implementation.
Factory Method Implementation
public static <T, V> Splitter<T, V> getSplitter(Class<T> key1, Class<V> key2) {
if (key1 == Company.class && key2 == Department.class)
return (Splitter<T, V>) new CompanySplitterImpl();
// more cases
}
Following is my call at client side which compiles fine
Splitter<Company, Department> split = getSplitter(Company.class, Department.class);
I want to avoid tight coupling of client side code with the implementation. Is there a way to avoid hardcoded Type params i.e. avoiding use Company and Department (Splitter<Company, Department>) at callee side and use some variable instead? Is there a way out through which they can be loaded from some external property file?
FYI : I am not sure about its feasibility in Java?
One thing you could do is have you factory not know anything about concrete implementations and instead have themselves register with it (or contain a predefined list) and ask each implementation if it can handle the types or not. For example, given a predefined list like your example above:
public class SplitterFactory {
private Set<Splitter> splitters = new HashSet<>() {{
add(new CompanySplitterImpl());
}};
public static <T, V> Splitter<T, V> getSplitter(Class<T> key1, Class<V> key2)
{
for (Splitter splitter : splitters) {
if (splitter.canAccept(key1, key2)) {
return splitter;
}
// no matched splitter
}
}
Obviously this is a very naive solution and you could implement the lookup more efficiently. If you don't know your types at compile time, you could also have a registration mechanism with the factory to include new ones at runtime. Because the Splitter itself is now responsible for reporting what types it can handle it's fully extensible.
You could make a simple map class that you can list them up to:
public final class SplitterMap {
private final List<SplitterType<?, ?>> list = new ArrayList<>();
private class SplitterType<T, V> {
private final Class<T> key1;
private final Class<V> key2;
private final Class<? extends Splitter<T, V>> clazz;
private SplitterType(Class<?> key1, Class<?> key2, Class<? extends Splitter<T, V> clazz) {
this.key1 = key1;
this.key2 = key2;
this.clazz = clazz;
}
private boolean matches(Class<?> key1, Class<?> key2) {
return this.key1 == key1 && this.key2 == key2;
}
}
public <T, V> void put(Class<T> key1, Class<V> key2, Class<? extends Splitter<T, V> clazz) {
list.add(new SplitterType<T, V>(key1, key2, clazz));
}
public <T, V> Splitter<T, V> get(Class<T> key1, Class<V> key2) {
for (SplitterType<?, ?> type : list) {
if (type.matches(key1, key2)) {
try {
return ((SplitterType<T, V>) type).clazz.newInstance();
} catch (Exception e) {
}
}
}
return null; // not found
}
}
Then you could just do:
SplitterMap map = new SplitterMap();
map.put(Company.class, Department.class, CompanySplitterImpl.class);
Splitter<Company, Department> splitter = map.get(Company.class, Department.class);
Not a good way but one way would be:
String companyClass = "Company";
String departmentClass = "Department";
Splitter split = getSplitter(Class.forName(companyClass), Class.forName(departmentClass));//raw splitter
System.out.println(split.split(new Company()));//you could use reflection here to create instance from companyClass String.
Firstly I assume you want something like
Splitter<Company, Department> s = Splitters.getSplitter()
Which is not posible without reflection, because of
type erasure
return type overloading not available in java yet
Secondly you're abusing FactoryMethod pattern. Which should look more like this:
interface Splitter<T, V> {
V[] split(T arg);
}
interface SplitterFactory {
<T, V> Splitter<T, V> getSplitter();
}
class CompanySplitterFactory implements SplitterFactory {
#Override
public Splitter<Company, Department> getSplitter() {
return new CompanySplitterImpl();
}
}
Background
An existing system creates a plethora of HashMap instances via its Generics class:
import java.util.Map;
import java.util.HashMap;
public class Generics {
public static <K,V> Map<K, V> newMap() {
return new HashMap<K,V>();
}
public static void main( String args[] ) {
Map<String, String> map = newMap();
}
}
This is the single point of creation for all instances of classes that implement the Map interface. We would like the ability to change the map implementation without recompiling the application. This would allow us to use Trove's THashMap, for example, to optimize the application.
Problem
The software cannot be bundled with Trove's THashMap due to licensing conditions. As such, it would be great if there was a way to specify the name of the map to instantiate at runtime (for those people who have no such licensing restrictions). For example:
import java.util.Map;
import java.util.HashMap;
import gnu.trove.map.hash.THashMap;
public class Generics {
private String mapClassName = "java.util.HashMap";
#SuppressWarnings("unchecked")
public <K,V> Map<K,V> newMap() {
Map<K,V> map;
try {
Class<? extends Map<K,V>> c = (Class<Map<K,V>>)Class.forName(
getMapClassName() ).asSubclass( Map.class );
map = c.newInstance();
}
catch( Exception e ) {
map = new HashMap<K,V>();
}
return map;
}
protected String getMapClassName() {
return this.mapClassName;
}
protected void setMapClassName( String s ) {
this.mapClassName = s;
}
public static void main( String args[] ) {
Generics g = new Generics();
Map<String, String> map = g.newMap();
System.out.printf( "Class = %s\n", map.getClass().toString() );
g.setMapClassName( "gnu.trove.map.hash.THashMap" );
map = g.newMap();
System.out.printf( "Class = %s\n", map.getClass().toString() );
}
}
Question
Is there a way to avoid the #SupressWarnings annotation when compiling with -Xlint and still avoid the warnings?
Is there a way to avoid the #SuppressWarnings annotation when compiling with -Xlint and still avoid the warnings?
No. Class.forName returns a Class<?>. The only way to assign it to Class<? extends Map<K, V>> is to do an unchecked cast. Sometimes unchecked casts are necessary, and so using #SuppressWarnings("unchecked") is acceptable here (provided you document the reason well).
IMHO it would be more correct to keep a reference to Class<? extends Map<?, ?>> and then do an unchecked cast of the result of newInstance to Map<K,V>. I only say this because a Class object is a canonical representation of a raw type, so a type like Class<? extends Map<K, V>> is slightly misleading.
This is outside the scope of the question, but here's a suggested alternative for your solution:
public interface MapFactory {
<K, V> Map<K, V> newMap() throws Exception;
}
public enum HashMapFactory implements MapFactory {
INSTANCE;
#Override
public <K, V> Map<K, V> newMap() {
return new HashMap<K, V>();
}
}
public static final class DynamicMapFactory implements MapFactory {
private final Constructor<? extends Map<?, ?>> constructor;
private DynamicMapFactory(Constructor<? extends Map<?, ?>> constructor) {
this.constructor = constructor;
}
#Override
//Impl note: these checked exceptions could also be wrapped in RuntimeException
public <K, V> Map<K, V> newMap() throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
#SuppressWarnings("unchecked") //this is okay because the default ctor will return an empty map
final Map<K, V> withNarrowedTypes = (Map<K, V>)constructor.newInstance();
return withNarrowedTypes;
}
public static DynamicMapFactory make(String className) throws ClassNotFoundException, NoSuchMethodException, SecurityException {
#SuppressWarnings("unchecked") //Class<? extends Map> can safely be cast to Class<? extends Map<?, ?>>
final Class<? extends Map<?, ?>> type =
(Class<? extends Map<?, ?>>)Class.forName(className).asSubclass(Map.class);
final Constructor<? extends Map<?, ?>> constructor = type.getDeclaredConstructor();
return new DynamicMapFactory(constructor);
}
}
public static void main(String[] args) throws Exception {
Map<String, Integer> map1 = HashMapFactory.INSTANCE.newMap();
Map<String, Integer> map2 = DynamicMapFactory.make("java.util.TreeMap").newMap();
System.out.println(map1.getClass()); //class java.util.HashMap
System.out.println(map2.getClass()); //class java.util.TreeMap
}