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));
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.
I have some special business that makes me need a custom serializer,it add a field to the field that modified the DictAnnotation annotation, It works.
But #JsonFormat not working.
My createTime field used to look like this:
"createTime":"2019-12-12"
now:
"createTime":1577835397615
The fields that are not modified by DictAnnotation have used the default JsonSerializer, so I don't know where the problem lies.
My main code is as follows:
bind custom SerializerModifier
#Bean
public Jackson2ObjectMapperBuilder objectMapperBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
SimpleModule simpleModule = new SimpleModule().setSerializerModifier(new DictSerializerModifier());
builder.modules(simpleModule);
return builder;
}
custome SerializerModifier
public class DictSerializerModifier extends Jdk8BeanSerializerModifier {
#Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
return new DictSerializer(null, (JsonSerializer<Object>) serializer);
}
}
custom Serializer
public class DictSerializer extends JsonSerializer<Object> implements ContextualSerializer {
private static final String MY_BIZ_FIELD_SUFFIX = "_test";
private DictAnnotation.ElementType type;
private JsonSerializer<Object> defSerializer;
public DictSerializer(DictAnnotation.ElementType type, JsonSerializer<Object> jsonSerializer) {
this.defSerializer = jsonSerializer;
this.type = type;
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
// my biz annotation
DictAnnotation.ElementType elementType = Optional.ofNullable(property).map(b -> b.getAnnotation(DictAnnotation.class))
.map(d -> d.type()).orElse(null);
// if null, use defSerializer
return elementType == null ? defSerializer : new DictSerializer(elementType, defSerializer);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
defSerializer.serialize(value, gen, serializers);
if (type != null) {
String fieldName = gen.getOutputContext().getCurrentName();
String codeLabel = RedisUtils.dictCodeToLabel(type, value.toString());
gen.writeStringField(fieldName.concat(MY_BIZ_FIELD_SUFFIX), codeLabel);
}
}
}
test pojo class
public class TestVo {
#DictAnnotation(type = A)
private String aCode;
#DictAnnotation(type = B)
private String bCode;
#DictAnnotation(type = C)
private String cCode;
#JsonFormat(pattern="yyyy-MM-dd",timezone = "GMT+8")
private Date createTime;
getter.... settter...
Looking forward to your guidance!
Can you please try if that helps (adding #JsonSerialize like mentioned below)
#JsonSerialize(as = Date.class)
#JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd")
private Date createTime;
I want to deserialize classes of the form:
public class TestFieldEncryptedMessage implements ITextMessage {
#JsonProperty("text")
#Encrypted(cipherAlias = "testAlias")
private String text;
public TestFieldEncryptedMessage() {
}
#JsonCreator
public TestFieldEncryptedMessage(#JsonProperty("text") String text) {
this.text = text;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
Where the text is encrypted and deserialization should unencrypt the value before rebuilding the TestFieldEncryptedMessage instance.
I am following an approach very similar to: https://github.com/codesqueak/jackson-json-crypto
That is, I am building a module extending SimpleModule:
public class CryptoModule extends SimpleModule {
public final static String GROUP_ID = "au.com.auspost.messaging";
public final static String ARTIFACT_ID = "jackson-json-crypto";
private EncryptedSerializerModifier serializerModifier;
private EncryptedDeserializerModifier deserializerModifier;
public CryptoModule() {
}
public CryptoModule addEncryptionService(final EncryptionService encryptionService) {
serializerModifier = new EncryptedSerializerModifier(encryptionService);
deserializerModifier = new EncryptedDeserializerModifier(encryptionService);
return this;
}
#Override
public String getModuleName() {
return ARTIFACT_ID;
}
#Override
public Version version() {
return new Version(major, minor, patch, null, GROUP_ID, ARTIFACT_ID);
}
#Override
public void setupModule(final SetupContext context) {
if ((null == serializerModifier) || (null == deserializerModifier))
throw new EncryptionException("Crypto module not initialised with an encryption service");
context.addBeanSerializerModifier(serializerModifier);
context.addBeanDeserializerModifier(deserializerModifier);
}
}
As you can see, two modifiers are set up: the EncryptedSerializerModifier works perfectly and is called by the ObjectMapper, but the deserializer behind the EncryptedDeserializerModifier is ignored.
As is seen in many other examples on SO such as here: How can I include raw JSON in an object using Jackson?, I set up the EncryptedDeserializerModifier with:
public class EncryptedDeserializerModifier extends BeanDeserializerModifier {
private final EncryptionService encryptionService;
private Map<String, SettableBeanProperty> properties = new HashMap<>();
public EncryptedDeserializerModifier(final EncryptionService encryptionService) {
this.encryptionService = encryptionService;
}
#Override
public BeanDeserializerBuilder updateBuilder(final DeserializationConfig config, final BeanDescription beanDescription, final BeanDeserializerBuilder builder) {
Encrypted annotation = beanDescription.getType().getRawClass().getAnnotation(Encrypted.class);
Iterator it = builder.getProperties();
while (it.hasNext()) {
SettableBeanProperty p = (SettableBeanProperty) it.next();
if (null != p.getAnnotation(Encrypted.class)) {
JsonDeserializer<Object> current = p.getValueDeserializer();
properties.put(p.getName(), p);
builder.addOrReplaceProperty(p.withValueDeserializer(new EncryptedJsonDeserializer(encryptionService, current, p)), true);
}
}
return builder;
}
}
Finally, the EncryptedJsonDeserializer itself overrides the following:
#Override
public Object deserialize(final JsonParser parser, final DeserializationContext context) throws JsonMappingException {
JsonDeserializer<?> deserializer = baseDeserializer;
if (deserializer instanceof ContextualDeserializer) {
deserializer = ((ContextualDeserializer) deserializer).createContextual(context, property);
}
return service.decrypt(parser, deserializer, context, property != null ? property.getType() : type);
}
#Override
public JsonDeserializer<?> createContextual(final DeserializationContext context, final BeanProperty property) throws JsonMappingException {
JsonDeserializer<?> wrapped = context.findRootValueDeserializer(property.getType());
return new EncryptedJsonDeserializer(service, wrapped, property);
}
The createContextual() method is called, but the deserialize method is not called. The property throughout the execution is always the "text" property, so I seem to have the right context.
anyone know why the ObjectMapper doesn't find the right Deserializer?
EDIT added implements ITextMessage to decrypted class, which I thought was an unimportant detail, but turned out to be the cause of the issue.
I found the issue! If you look closely at the TestFieldEncryptedMessage class, whose text field is encrypted, you can see that it implements an interface. The interface is used so that the messages give some extra tooling for asserts in tests, however for deserialization, there is an unintended consequence. When the ObjectMapper is working its way through the json string, it tries, I think, to match a deserializer to a field inside ITextMessage, not to a field inside TestFieldEncryptedMessage, which is why the custom deserializer was not called (there is no text field in ITextMessage).
Once I stopped implementing ITextMessage, the custom deserializer was called.
I have an Socket-Server in Java. This socket will receive json-strings with an specific structure.
{
"command": "test",
"name": "Hallo Welt"
}
I can not change this structure. The value of "command" will declare the type of content.
After I receive this from the socket, I would like to call different handlers, to handle these different commands:
command "test" > TestHandler implements CommandHandler
command "foo" > FooHandler implements CommandHandler
How can I convert the json into a object and bind the object to the specific handler?
This is my current approach:
I have an model class called BaseCommand which contains a enum command field.
class BaseCommand {
public CommandType command;
}
class TestCommand extends BaseCommand {
public String name;
}
With GSON I parse the JSON to BaseCommand class.
After that I can read the command type.
I declare a ENUM to map the command types to the Handler:
enum CommandType {
test(TestHandler.class),
foo(FooHandler.class);
public final Class<? extends CommandHandler> handlerClass;
public CommandTypes(Class<? extends CommandHandler> handlerClass) {
this.handlerClass = handlerClass;
}
}
My handler's are implementing this interface:
public interface CommandHandler<T extends BaseCommand> {
void handle(T command);
}
Now I have the command type enum and through Google Guices MapBinder I can get the Handler instance to handle request. This works
// in class ...
private final Map<CommandType, CommandHandler> handlers;
#Inject ClassName(Map<CommandType, CommandHandler> handlers) {
this.handlers = handlers;
}
// in converter method
private void convert(String json) {
BaseCommand baseCommand = GSONHelper().fromJson(json, BaseCommand.class);
// How can I get the CommandModel?
// If the commandType is "test" how can I parse TestCommand automatically?
??? commandModel = GSONHelper().fromJson(json, ???);
handlers.get(baseCommand.command).handle(commandModel);
}
Does anyone know a solution for my problem?
Or a complete different approach for this?
best regards, Michael
How can I get the CommandModel?
If the commandType is "test" how can I parse TestCommand automatically?
You can use a TypeAdapterFactory to get the most appropriate type adapter in the most accurate and flexible way. The example below slightly differs from your classes naming, but I think it's not a big issue to you. So, let's assume you have the following command arguments DTO declarations:
abstract class AbstractCommandDto {
final String command = null;
}
final class HelloCommandDto
extends AbstractCommandDto {
final String name = null;
}
Now you can make a special TypeAdapterFactory to make a sort of looking-ahead to determine the incoming command by command arguments name. It may look complicated, but in fact TypeAdapterFactoryies are not that hard to implement. Note that JsonDeserializer might be another option for you, but then you lose automatic deserializing unless you delegate its deserialize() method to another backing Gson instance.
final class AbstractCommandDtoTypeAdapterFactory
implements TypeAdapterFactory {
// The factory handles no state and can be instantiated once
private static final TypeAdapterFactory abstractCommandDtoTypeAdapterFactory = new AbstractCommandDtoTypeAdapterFactory();
// Type tokens are used to define type information and are perfect value types so they can be instantiated once as well
private static final TypeToken<CommandProbingDto> abstractCommandProbingDtoTypeToken = new TypeToken<CommandProbingDto>() {
};
private static final TypeToken<HelloCommandDto> helloCommandDtoTypeToken = new TypeToken<HelloCommandDto>() {
};
private AbstractCommandDtoTypeAdapterFactory() {
}
static TypeAdapterFactory getAbstractCommandDtoTypeAdapterFactory() {
return abstractCommandDtoTypeAdapterFactory;
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// First, check if the incoming type is AbstractCommandDto
if ( AbstractCommandDto.class.isAssignableFrom(typeToken.getRawType()) ) {
// If yes, then build a special type adapter for the concrete type
final TypeAdapter<AbstractCommandDto> abstractCommandDtoTypeAdapter = new AbstractCommandDtoTypeAdapter(
gson,
gson.getDelegateAdapter(this, abstractCommandProbingDtoTypeToken),
(commandName, jsonObject) -> deserialize(gson, commandName, jsonObject),
dto -> getTypeAdapter(gson, dto)
);
// Some cheating for javac...
#SuppressWarnings("unchecked")
final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) abstractCommandDtoTypeAdapter;
return typeAdapter;
}
// If it's something else, just let Gson pick up the next type adapter
return null;
}
// Create an AbstractCommandDto instance out of a ready to use JsonObject (see the disadvantages about JSON trees below)
private AbstractCommandDto deserialize(final Gson gson, final String commandName, final JsonObject jsonObject) {
#SuppressWarnings("unchecked")
final TypeToken<AbstractCommandDto> typeToken = (TypeToken<AbstractCommandDto>) resolve(commandName);
final TypeAdapter<AbstractCommandDto> typeAdapter = gson.getDelegateAdapter(this, typeToken);
return typeAdapter.fromJsonTree(jsonObject);
}
private TypeAdapter<AbstractCommandDto> getTypeAdapter(final Gson gson, final AbstractCommandDto dto) {
#SuppressWarnings("unchecked")
final Class<AbstractCommandDto> clazz = (Class<AbstractCommandDto>) dto.getClass();
return gson.getDelegateAdapter(this, TypeToken.get(clazz));
}
// Or any other way to resolve the class. This is just for simplicity and can be even extract elsewhere from the type adapter factory class
private static TypeToken<? extends AbstractCommandDto> resolve(final String commandName)
throws IllegalArgumentException {
switch ( commandName ) {
case "hello":
return helloCommandDtoTypeToken;
default:
throw new IllegalArgumentException("Cannot handle " + commandName);
}
}
private static final class AbstractCommandDtoTypeAdapter
extends TypeAdapter<AbstractCommandDto> {
private final Gson gson;
private final TypeAdapter<CommandProbingDto> probingTypeAdapter;
private final BiFunction<? super String, ? super JsonObject, ? extends AbstractCommandDto> commandNameToCommand;
private final Function<? super AbstractCommandDto, ? extends TypeAdapter<AbstractCommandDto>> commandToTypeAdapter;
private AbstractCommandDtoTypeAdapter(
final Gson gson,
final TypeAdapter<CommandProbingDto> probingTypeAdapter,
final BiFunction<? super String, ? super JsonObject, ? extends AbstractCommandDto> commandNameToCommand,
final Function<? super AbstractCommandDto, ? extends TypeAdapter<AbstractCommandDto>> commandToTypeAdapter
) {
this.gson = gson;
this.probingTypeAdapter = probingTypeAdapter;
this.commandNameToCommand = commandNameToCommand;
this.commandToTypeAdapter = commandToTypeAdapter;
}
#Override
public void write(final JsonWriter out, final AbstractCommandDto dto)
throws IOException {
// Just pick up a delegated type adapter factory and use it
// Or just throw an UnsupportedOperationException if you're not going to serialize command arguments
final TypeAdapter<AbstractCommandDto> typeAdapter = commandToTypeAdapter.apply(dto);
typeAdapter.write(out, dto);
}
#Override
public AbstractCommandDto read(final JsonReader in) {
// Here you can two ways:
// * Either "cache" the whole JSON tree into memory (JsonElement, etc,) and simplify the command peeking
// * Or analyze the JSON token stream in a more efficient and sophisticated way
final JsonObject jsonObject = gson.fromJson(in, JsonObject.class);
final CommandProbingDto commandProbingDto = probingTypeAdapter.fromJsonTree(jsonObject);
// Or just jsonObject.get("command") and even throw abstractCommandDto, AbstractCommandProbingDto and all of it gets away
final String commandName = commandProbingDto.command;
return commandNameToCommand.apply(commandName, jsonObject);
}
}
// A synthetic class just to obtain the command field
// Gson cannot instantiate abstract classes like what AbstractCommandDto is
private static final class CommandProbingDto
extends AbstractCommandDto {
}
}
And how it's used:
public static void main(final String... args) {
// Build a command DTO-aware Gson instance
final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(getAbstractCommandDtoTypeAdapterFactory())
.create();
// Build command registry
final Map<Class<?>, Consumer<?>> commandRegistry = new LinkedHashMap<>();
commandRegistry.put(HelloCommandDto.class, new HelloCommand());
// Simulate and accept a request
final AbstractCommandDto abstractCommandDto = gson.fromJson("{\"command\":\"hello\",\"name\":\"Welt\"}", AbstractCommandDto.class);
// Resolve a command
final Consumer<?> command = commandRegistry.get(abstractCommandDto.getClass());
if ( command == null ) {
throw new IllegalArgumentException("Cannot handle " + abstractCommandDto.command);
}
// Dispatch
#SuppressWarnings("unchecked")
final Consumer<AbstractCommandDto> castCommand = (Consumer<AbstractCommandDto>) command;
castCommand.accept(abstractCommandDto);
// Simulate a response
System.out.println(gson.toJson(abstractCommandDto));
}
private static final class HelloCommand
implements Consumer<HelloCommandDto> {
#Override
public void accept(final HelloCommandDto helloCommandDto) {
System.out.println("Hallo " + helloCommandDto.name);
}
}
The output:
Hallo Welt
I have three classes which inherits from a super class (SensorData)
#JsonDeserialize(using = SensorDataDeserializer.class)
public abstract class SensorData {
}
public class HumiditySensorData extends SensorData {
}
public class LuminositySensorData extends SensorData {
}
public class TemperatureSensorData extends SensorData {
}
I want convert a json input into one of this classes depending on a parameter. I'm trying to use Jackson StdDeserializer and I create a custom deserializer
#Component
public class SensorDataDeserializer extends StdDeserializer<SensorData> {
private static final long serialVersionUID = 3625068688939160875L;
#Autowired
private SensorManager sensorManager;
private static final String discriminator = "name";
public SensorDataDeserializer() {
super(SensorData.class);
SpringBeanProvider.getInstance().autowireBean(this);
}
#Override
public SensorData deserialize(JsonParser parser,
DeserializationContext context) throws IOException,
JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
ObjectNode root = (ObjectNode) mapper.readTree(parser);
ObjectNode sensor = (ObjectNode) root.get("data");
String type = root.get(discriminator).asText();
Class<? extends SensorData> clazz = this.sensorManager
.getCachedSensorsMap().get(type).sensorDataClass();
if (clazz == null) {
// TODO should throw exception
return null;
}
return mapper.readValue(sensor.traverse(), clazz);
}
}
My problem is that when I determine the correct type to mapping the concrete class, the mapper call again to the custom StdDeserializer. So I need a way
to broke the cycle when I have the correct type. The stacktrace is the next one
java.lang.NullPointerException
at com.hp.psiot.mapping.SensorDataDeserializer.deserialize(SensorDataDeserializer.java:38)
at com.hp.psiot.mapping.SensorDataDeserializer.deserialize(SensorDataDeserializer.java:1)
at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:3532)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:1868)
at com.hp.psiot.mapping.SensorDataDeserializer.deserialize(SensorDataDeserializer.java:47)
at com.hp.psiot.mapping.SensorDataDeserializer.deserialize(SensorDataDeserializer.java:1)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3560)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2660)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:205)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:200)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters (AbstractMessageConverterMethodArgumentResolver.java:138)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:184)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:105)
An example of input
{
"name":"temperature",
"data": {
"value":20
}
}
I only include the stacktrace to show that the mapper is calling again to the deserializer. The reason for the nullPointerException is that when the second the ObjectMapper is called the input is
"value":20
So, An exception is threw because we don't have the information to determine the type and it doesn't check if the input is correct
I want to avoid using JsonSubTypes and JsonTypeInfo if it's posible.
Thanks in advance!
Partial solution
In my case the SensorData is wrapped in other class (ServiceData)
class ServiceData {
#JsonDeserialize(using = SensorDataDeserializer.class)
List<SensorData> sensors;
}
So, I get rid of JsonDeserializer in SensorData class and put it in the field avoiding the cycle. The solution isn't the best, but in my case it helps me. But in the case that the class isn't wrapped in another one we still have the same problem.
Note that if you have a Collection and you annotate with JsonDeserialize that field you have to handle all the collection. Here is the modification
in my case
#Component
public class SensorDataDeserializer extends StdDeserializer<List<SensorData>> {
private static final long serialVersionUID = 3625068688939160875L;
#Autowired
private SensorManager sensorManager;
private static final String discriminator = "name";
public SensorDataDeserializer() {
super(SensorData.class);
SpringBeanProvider.getInstance().autowireBean(this);
}
#Override
public List<SensorData> deserialize(JsonParser parser,
DeserializationContext context) throws IOException,
JsonProcessingException {
try {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
ArrayNode root = (ArrayNode) mapper.readTree(parser);
int size = root.size();
List<SensorData> sensors = new ArrayList<SensorData>();
for (int i = 0; i < size; ++i) {
ObjectNode sensorHead = (ObjectNode) root.get(i);
ObjectNode sensorData = (ObjectNode) sensorHead.get("data");
String tag = sensorHead.get(discriminator).asText();
Class<? extends SensorData> clazz = this.sensorManager
.getCachedSensorsMap().get(tag).sensorDataClass();
if (clazz == null) {
throw new InvalidJson("unbound sensor");
}
SensorData parsed = mapper.readValue(sensorData.traverse(),
clazz);
if (parsed == null) {
throw new InvalidJson("unbound sensor");
}
sensors.add(parsed);
}
return sensors;
} catch (Throwable e) {
throw new InvalidJson("invalid data");
}
}
}
Hope it helps someone :)
Why don't you just use #JsonTypeInfo? Polymorphic handling is the specific use case for it.
In this case, you would want to use something like:
#JsonTypeInfo(use=Id.NAME, include=As.PROPERTY, property="name")
#JsonSubTypes({ HumiditySensorData.class, ... }) // or register via mapper
public abstract class SensorData { ... }
#JsonTypeName("temperature")
public class TemperaratureSensorData extends SensorData {
public TemperaratureSensorData(#JsonProperty("data") JsonNode data) {
// extract pieces out
}
}
which would handle resolution from 'name' into sub-type, bind contents of 'data' as JsonNode (or, if you prefer can use Map or Object or whatever type matches).