Reputation: 17496
I have a JSON file like this:
{
"Properties": {
"String": "one-string-value",
"Number": 123,
"LiteralList": [
"first-value",
"second-value"
],
"Boolean": true,
"ReferenceForOneValue": {
"Ref": "MyLogicalResourceName"
}
}
}
I want to use Jackson to deserialize it into a proper Map<String,Property>
. For this i'm using TypeReference
like so:
public class Template {
@JsonProperty("Properties")
@JsonDeserialize(using = PropertyDeserializer.class)
public Map<String, Object> propertyList;
}
public class PropertyDeserializer extends JsonDeserializer<Map<String, Property>> {
public Map<String, Property> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
Map<String,Property> result =
mapper.readValue(jsonParser, new TypeReference<Map<String,Property>>() {});
return result;
}
}
Each Property
can only take some values: strings, ints, booleans, a list of strings, or another object which has one key "Ref". So I wrote this:
public class Property {
private Object value;
public Property(String value) {
this.value = value;
}
public Property(int value) {
this.value = value;
}
public Property(List<String> values) {
this.value = values;
}
public Property(boolean value) {
this.value = value;
}
public Property(Ref reference) {
this.value = reference;
}
}
public class Ref {
private String reference;
@JsonCreator
public Ref(@JsonProperty("Ref") String reference) {
this.reference = reference;
}
}
However, this doesn't work:
For the LiteralList
property, I get the error Cannot deserialize instance of Property
out of START_ARRAY token
For the ReferenceForOneValue
property, I get the error Cannot construct instance of Property
(although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
I have uploaded all the source code here.
Can someone help me write a deserializer for this?
Upvotes: 0
Views: 3267
Reputation: 413
In my experience Jackson doesn't like the whole List interface.
public class Property{
//...
public Property(String[] list){
this.value = list;
}
//...
or if you WANT a List object:
public class Property{
//...
public Property(String[] list){
this.value = Arrays.asList(list);
}
//...
or simply go with @cassiomolin's answer
Upvotes: 0
Reputation: 18235
A valid approach is to write your custom creator:
@JsonCreator
public Property(JsonNode node) {
switch (node.getNodeType()) {
case STRING:
value = node.asText();
break;
case NUMBER:
value = node.asInt();
break;
case BOOLEAN:
value = node.asBoolean();
break;
case ARRAY:
Iterator<JsonNode> elements = node.elements();
List<String> list = new ArrayList<>();
while (elements.hasNext()) {
JsonNode element = elements.next();
if (element.isTextual()) {
list.add(element.textValue());
} else {
throw new RuntimeException("invalid data type");
}
}
value = list;
break;
case OBJECT:
try {
value = new ObjectMapper().treeToValue(node, Ref.class);
} catch (JsonProcessingException e) {
throw new RuntimeException("invalid data type", e);
}
break;
default:
throw new RuntimeException("invalid data type");
}
}
Upvotes: 4
Reputation: 14328
Here is my solution, it uses Java reflection to match property constructor to json data type. I had to modify the property constructors a bit to match Jackson deserialization (use wrapper types instead of primitives). Also, reference type is built from Map (Reference
class is stripped of all Jackson annotations)
Property class:
import java.util.List;
import java.util.Map;
public class Property {
private Object value;
public Property(String value) {
this.value = value;
}
public Property(Integer value) {
this.value = value;
}
public Property(List<String> values) {
this.value = values;
}
public Property(Boolean value) {
this.value = value;
}
public Property(Ref reference) { // not used
this.value = reference;
}
// reference is received as map
public Property(Map<String, String> map) {
if (map.containsKey("Ref")) {
this.value = new Ref(map.get("Ref"));
} else {
throw new IllegalArgumentException("invalid reference property");
}
}
@Override
public String toString() {
return value.toString();
}
}
Template class
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class Template {
@JsonIgnore
public Map<String, Property> propertyList = new HashMap<>();
// map of data type from json to property constructor
private static Map<Class<?>, Constructor<Property>> propertyConstructors;
// build propertyConstructors map
static {
propertyConstructors = new HashMap<>();
@SuppressWarnings("unchecked")
Constructor<Property>[] constructors = (Constructor<Property>[])Property.class.getConstructors();
for (Constructor<Property> ctor : constructors) {
if (ctor.getParameterCount() == 1) {
propertyConstructors.put(ctor.getParameterTypes()[0], ctor);
}
}
}
@JsonProperty("Properties")
public void setProperties(Map<String, Object> jsonProperties) {
if (jsonProperties == null || jsonProperties.isEmpty()) return;
for (Map.Entry<String, Object> property : jsonProperties.entrySet()) {
Optional<Constructor<Property>> optionalCtor =
propertyConstructors.keySet().stream()
.filter(argType -> argType.isAssignableFrom(property.getValue().getClass()))
.map(argType -> propertyConstructors.get(argType))
.findFirst();
if (optionalCtor.isPresent()) {
try {
propertyList.put(property.getKey(), optionalCtor.get().newInstance(property.getValue()));
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException("invalid property " + property.getKey());
}
}
}
}
}
the deserialization is straightforward:
Template t = mapper.readValue(in, Template.class);
Upvotes: 1