I have a class that extends JsonDerserializer<Type>. In this deserialiser I have a concept of doing replacement values which I currently set using objectReader(Injectables). My problem is that sometimes I don't have injectables.
I don't see a method on ObjectMapper that allows me to check if an injectable key is set. I only see findInjectableValue which if the value isn't there it throws an InvalidDefinitionException. I am currently try catching this call which works, but I feel it is more of a hack.
Is there something I am missing?
I really don't want to have this try-catch. I want to first check if injectable value exists.
try {
Object replacementValueObject = ctxt.findInjectableValue("replacementValues", null, null);
if (replacementValueObject instanceof Map) {
replacementValues = (Map<String, Object>) replacementValueObject;
mapper.setInjectableValues(new InjectableValues.Std().addValue("replacementValues", replacementValues));
}
}catch (InvalidDefinitionException ie){
logger.info("No replacement values exist. Ignoring and moving on");
}
Right now (07-2019), there is no method to check whether value exists or not. You can only addValue and try to get it by invoking findInjectableValue.
You can create empty Map and initialise InjectableValues when ObjectMapper is created:
InjectableValues.Std injectableValues = new InjectableValues.Std();
injectableValues.addValue("replacementValues", Collections.emptyMap());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setInjectableValues(injectableValues);
or create nice method which hides complex logic behind scene:
class ReplaceMapValueJsonDeserializer extends JsonDeserializer<Object> {
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) {
return null;
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) throws IOException {
Map replacementValues = getCurrentReplacementValues(ctxt);
//...
return super.deserialize(p, ctxt, intoValue);
}
private Map<String, Object> getCurrentReplacementValues(DeserializationContext ctxt) {
try {
Object value = ctxt.findInjectableValue("replacementValues", null, null);
return (Map<String, Object>) value;
} catch (JsonMappingException ie) {
return Collections.emptyMap();
}
}
}
See also:
How to use injection with XmlMapper deserialization
Related
I have a class that accept a function Function<ByteBuffer, T> deserialize as a constructor argument.
I want to create a function which converts JSON into a list of objects.
Here's what I am trying to do:
public Function<ByteBuffer, List<MyObject>> deserialize(
final ObjectMapper objectMapper) {
return objectMapper.readValue(??, new TypeReference<List<MyObject>>(){});
}
Obviously the syntax is wrong here. How can I fix this?
Instead of creating such a function I would rather go with utility method because ObjectMapper.readValue() throws IOException which is checked. Therefore it should be handled because we can't propagate checked exceptions outside the Function.
If you wonder how it might look like, here it is:
public Function<ByteBuffer, List<MyObject>> deserialize(ObjectMapper objectMapper) {
return buffer -> {
try {
return objectMapper.readValue(buffer.array(),new TypeReference<List<MyObject>>() {
});
} catch (IOException e) {
e.printStackTrace();
return null;
}
};
}
Instead, let's consider a utility-class:
public static final ObjectMapper objectMapper = // initializing the mapper
public static List<MyObject> deserialize(ByteBuffer buffer) throws IOException {
return objectMapper.readValue(buffer.array(),new TypeReference<>() {});
}
I'm trying to deserialize a Json into an existing instance in my process. So it only creates a new instance if none exists. Alls objects contains an id to Identifiy them.
I used this answer: https://stackoverflow.com/a/18405958/11584969 and tried to create a custon Deserializer for this.
So far I have managed to create a custon Deserializer which checks for existing instances, but I was not able to fill the new instance or change the existing one.
My deserialize function is:
public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
if (node instanceof NullNode) {
return null;
}
// get id from node
String strId = node.get("id").asText();
UUID id = UUID.fromString(strId);
// search for existing instance or create it
T mObject = ...
// fill/change instance
return (T) defaultDeserializer.deserialize(jp, ctxt, mObject);
}
The object mapper creation:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping();
objectMapper.registerModule(new Jdk8Module());
objectMapper.registerModule(new JavaTimeModule());
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (beanDesc.getBeanClass() == Table.class)
return new ModelObjectDeserializer<>(Table.class, (JsonDeserializer<Table>) deserializer);
return deserializer;
}
});
objectMapper.registerModule(module);
The code above runs without any error or exception but the instance from mObject is not filled by defaultDeserializer.deserialize(jp, ctxt, mObject);
If I don't use my custom deserializer, the created instances are filled as expected.
It is not quite an answer to the question, but my initial goal was:
'm trying to deserialize a Json into an existing instance in my process. So it only creates a new instance if none exists. Alls objects contains an id to Identifiy them.
For everyone who tries to accomplish the same, here is how I implemented it:
public class ModelInstantiator extends StdValueInstantiator {
private static final long serialVersionUID = -7760885448565898117L;
private Class<? extends ModelObject> clazz;
/**
* #param config
* #param valueType
*/
public ModelInstantiator(DeserializationConfig config, Class<? extends ModelObject> clazz) {
super(config, config.constructType(clazz));
this.clazz = clazz;
}
#Override
public boolean canCreateFromObjectWith() {
return true;
}
#Override
public Object createFromObjectWith(DeserializationContext ctxt, Object[] args) throws IOException, JsonProcessingException {
UUID id = (UUID) args[0];
// get local object
ModelObject object = ...
// if id was not found => create and add
if (object == null) {
try {
object = clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new IOException(e);
}
object.setId(id);
// add to local list
...
}
return object;
}
#Override
public SettableBeanProperty[] getFromObjectArguments(DeserializationConfig config) {
CreatorProperty idProp = new CreatorProperty(new PropertyName("id"), config.constructType(UUID.class), null, null, null, null,
0, null, PropertyMetadata.STD_REQUIRED);
return new SettableBeanProperty[] { idProp };
}
}
I had to split the local and json id. Ohterwise the id in the array is null.
Given
public class ConstraintMatch {
protected String constraintName;
protected Score score;
...
}
I have the following serializer in Jackson:
public class ConstraintMatchJacksonJsonSerializer extends JsonSerializer<ConstraintMatch> {
#Override
public void serialize(ConstraintMatch constraintMatch, JsonGenerator generator, SerializerProvider serializers)
throws IOException {
generator.writeStartObject();
generator.writeStringField("constraintName", constraintMatch.getConstraintName());
generator.writeFieldName("score");
// Delegate to serialization to the default Score serializer
serializers.findValueSerializer(Score.class)
.serialize(constraintMatch.getScore(), generator, serializers);
generator.writeEndObject();
}
}
How do I write a deserializer that also delegates to the default deserializer?
public class ConstraintMatchJacksonJsonDeserializer extends JsonDeserializer<ConstraintMatch> {
#Override
public ConstraintMatch deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonNode tree = parser.readValueAsTree();
String constraintName = tree.get("constraintName").asText();
JsonNode scoreNode = tree.get("score");
Score score = ...; // How do I delegate to the default deserializer?
return new ConstraintMatch(constraintName, score);
}
}
I've looked at findContextualValueDeserializer() etc, but I can't create a BeanProperty instance.
In a similar situation, I actually found there were two problems to solve. Firstly, as you say, the need to delegate back to the normal deserializer. But the other problem I encountered was how to feed the JsonNode (TreeNode below) into that next deserialize(JsonParser, ...).
The following is a working sample from that situation, where I wanted to do a lookahead to figure out the subclass.
Hopefully the node here is your scoreNode. And it sounds like objectClass is just Score.class for you.
#Override
public T deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
TreeNode node = parser.readValueAsTree();
// Select the subclass to deserialize as
Class<? extends T> objectClass = deduceClass(node);
// This based on ObjectMapper._convert()
// - the problem here was the JsonParser (parser) had gone past the current node
TokenBuffer buf = new TokenBuffer(mapper, false);
SerializationConfig config = mapper.getSerializationConfig()
.without(SerializationFeature.WRAP_ROOT_VALUE);
DefaultSerializerProvider serializerProvider = ((DefaultSerializerProvider) mapper
.getSerializerProvider()).createInstance(config,
mapper.getSerializerFactory());
serializerProvider.serializeValue(buf, node);
JsonParser nestedParser = buf.asParser();
nestedParser.nextToken();
JsonDeserializer<Object> deserializer = ctxt
.findRootValueDeserializer(
mapper.getTypeFactory().constructType(objectClass));
#SuppressWarnings("unchecked")
T obj = (T) deserializer.deserialize(nestedParser, ctxt);
return obj;
}
(Just in case, this was with Jackson 2.7.9)
I'd be pleased to hear about a simpler way to create a JsonParser from a node.
Serializing this:
constraintMatch.getConstraintPackage());
generator.writeStringField("constraintName", constraintMatch.getConstraintName());
generator.writeFieldName("score");
// Delegate to PolymorphicScoreJacksonJsonSerializer
JsonSerializer<Object> scoreSerializer = serializers.findValueSerializer(Score.class);
scoreSerializer.serialize(constraintMatch.getScore(), generator, serializers);
generator.writeEndObject();
Can be deserialized with this:
parser.nextToken();
if (!"constraintName".equals(parser.getCurrentName())) {
throw new IllegalStateException(...);
}
parser.nextToken();
String constraintName = parser.getValueAsString();
parser.nextToken();
if (!"score".equals(parser.getCurrentName())) {
throw new IllegalStateException(...);
}
parser.nextToken();
JsonDeserializer<Object> scoreDeserializer = context.findNonContextualValueDeserializer(context.constructType(Score.class));
Score score = (Score) scoreDeserializer.deserialize(parser, context);
My server return results having 2 different types: SearchResultDTO when successefull and String when error. I need to handle these 2 types and return always SearchResultDTO type. Here is my deserializer:
public class SearchResultsDeserializer extends JsonDeserializer<SearchResultDTO> {
#Override
public SearchResultDTO deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
JsonToken t = jp.getCurrentToken();
if (t == JsonToken.VALUE_STRING){
return new SearchResultDTO(jp.getText());
} else {
return jp.readValueAs(SearchResultDTO.class);
}
}
}
When i run this code and server send SearchResultDTO object, jackson go in infinite loop by calling this function and returns with error: "java.lang.StackOverflowError: stack size 1036KB"
Easy fix would be to create new instance of ObjectMapper in your SearchResultsDeserializer and use it instead of JsonParser
public static class SearchResultsDeserializer extends JsonDeserializer<SearchResultDTO> {
private static ObjectMapper mapper = new ObjectMapper();
#Override
public SearchResultDTO deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
JsonToken t = jp.getCurrentToken();
if (t == JsonToken.VALUE_STRING){
return new SearchResultDTO(jp.getText());
} else {
return mapper.readValue(jp, SearchResultDTO.class);
}
}
}
The hard way is to save default deserializer in your custom deserializer.
Here is more info: How do I call the default deserializer from a custom deserializer in Jackson
I need to deserialize some json which can contain either an array of objects [{},{}] or a single object {}. See my question. Here is what I'm trying to do :
public class LocationDeserializer extends JsonDeserializer<List<Location>>{
#Override
public List<Location> deserialize(JsonParser jp,
DeserializationContext ctxt) throws IOException
{
List<Location> list = new ArrayList<Location>();
if(!jp.isExpectedStartArrayToken()){
list.add(...);
}else{
//Populate the list
}
return list;
}
But I'm getting stuck here. How can I remap the object? And how to tell Jackson to use this deserializer for the attribute "location"?
Here is how the Json can look :
{
"location":
[
{
"code":"75",
"type":"1"
},
{
"code":"77",
"type":"1"
}
]
}
or
{
"location":
{
"code":"75",
"type":"1"
}
}
You can tell Jackson to use this deserializer with the Annotation JsonDeserialize.
And inside your deserialize method, you could use the following:
#Override
public List<Location> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
List<Location> list = new ArrayList<Location>();
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(jp);
if(root.get("location").isArray()){
// handle the array
}else{
// handle the single object
}
return list;
}
I don't know what your JSON looks like, but I think using ObjectNode is a lot easier for this case than using JsonDeserializer. Something like this:
ObjectNode root = mapper.readTree("location.json");
if (root.getNodeType() == JsonNodeType.ARRAY) {
//Use a get and the JsonNode API to traverse the tree to generate List<Location>
}
else {
//Use a get and the JsonNode API to traverse the tree to generate single Location or a one-element List<Location>
}