Jackson: Override primitive type deserialization? - java

We need to process some broken JSON from a legacy server here that wrongly encodes null values as literal "null" strings in its output.
I already found that I probably want to override https://github.com/FasterXML/jackson-core/blob/master/src/main/java/com/fasterxml/jackson/core/base/ParserMinimalBase.java#L368 to "fix" this, but this seems to be so deep inside Jackson that I'd rather do it differently. Are there alternatives, for example by using the ObjectMapper to add a custom deserializer for the String.class or am I lost?

Ok, it worked by overriding the standard String deserializer. Unfortunately I had to copy the complete implementation over because org/codehaus/jackson/map/deser/std/StringDeserializer.java is final and cannot be extended.
public class FixesModule extends SimpleModule {
public FixesModule() {
super();
addDeserializer(String.class, new CustomStringDeserializer());
}
}
and
public class CustomStringDeserializer extends StdScalarDeserializer<String> {
private static final String NULL_STRING = "null";
public CustomStringDeserializer() {
super(String.class);
}
#Override
public String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonToken curr = jp.getCurrentToken();
// Usually should just get string value:
if (curr == JsonToken.VALUE_STRING) {
// BEGIN NULL_STRING fix
if (NULL_STRING.equals(jp.getText())) {
return null;
}
// END NULL_STRING fix
return jp.getText();
}
// [JACKSON-330]: need to gracefully handle byte[] data, as base64
if (curr == JsonToken.VALUE_EMBEDDED_OBJECT) {
Object ob = jp.getEmbeddedObject();
if (ob == null) {
return null;
}
if (ob instanceof byte[]) {
return Base64Variants.getDefaultVariant().encode((byte[]) ob, false);
}
// otherwise, try conversion using toString()...
return ob.toString();
}
// Can deserialize any scalar value, but not markers
if (curr.isScalarValue()) {
return jp.getText();
}
throw ctxt.mappingException(_valueClass, curr);
}
// 1.6: since we can never have type info ("natural type"; String, Boolean,
// Integer, Double):
// (is it an error to even call this version?)
#Override
public String deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer)
throws IOException, JsonProcessingException {
return deserialize(jp, ctxt);
}
}

Related

Handling boolean values and empty strings for a property in Jackson?

I have a JSON property that can be one of
{ "observed": true }
{ "observed": false }
{ "observed": "" }
I'd like to map it so that in Java it will one of "true", "false" or ""
#JsonProperty("observed")
private String observedValue;
Then I'll just make a getter that would give me a
public Optional<Boolean> getObservedOpt() {
if ("".equals(observedValue)) {
return Optional.empty();
} else {
return Optional.of(Boolean.parseBoolean(observedValue));
}
}
However, I am not sure how to make it converted true and false into strings. Or perhaps there's a more elegant way of doing it without the string comparison.
I would suggest configure object mapper with this feature ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, so in case of empty string it will be assigned to null
ObjectMapper mapper = new ObjectMapper()
.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
And you can happily declare this field as Boolean type, be aware in case of empty string this field value will be null
#JsonProperty("observed")
private Boolean observedValue;
One solution could be using Custom JsonDeserializer
public static class StringBooleanDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser parser, DeserializationContext context) throws IOException {
if(parser.getValueAsBoolean()){
return "true";
}else{
if(parser.getTextLength()==0) {
return parser.getText();
}else{
return "false";
}
}
}
}
#JsonProperty("observed")
#JsonDeserialize(using=StringBooleanDeserializer.class)
private String observedValue;
Similarly, you can also write custom JSON Serializer.

Json deserializer that works across different classes in java

I am trying to create an #JsonDeserializer that will work across classes. I am using JAX-RS and the incoming json string will have fields in snake case. I want to override the json deserialization so that my java objects do not have snake-case fields. Since the creation of the java object is happening within JAX-RS, I am using the #JsonDeserializer annotation on all my request classes. My current implementation has a generic base class, but I need to extend it for all the concrete classes so that I can pass in the actual class I want to create. Is there any way to do this more generically?
For example, I have multiple request objects like this:
#JsonDeserialize(using = MyRequestDeserializer.class)
public class MyRequest {
....
}
I have created a generic deserializer like so:
public class GenericRequestDeserializer extends JsonDeserializer<Object> {
private static ObjectMapper objectMapper = new ObjectMapper();
#Override
public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
return null;
}
protected Object deserializeIt(JsonParser jsonParser, Class cls) {
try {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Iterator<String> fieldNames = node.fieldNames();
Object object = cls.newInstance();
while(fieldNames.hasNext()) {
String fieldName = fieldNames.next();
JsonNode value = node.get(fieldName);
String newFieldName = convertFieldName(fieldName);
//TODO: currently failing if I do not find a field, should the exception be swallowed?
Class returnType = object.getClass().getMethod("get" + newFieldName).getReturnType();
Method setMethod = object.getClass().getMethod("set" + newFieldName, returnType);
Object valueToSet = null;
if(value.isTextual()) {
valueToSet = value.asText();
} else if(value.isContainerNode()) {
valueToSet = objectMapper.readValue(value.toString(), returnType);
} else if (value.isInt()) {
valueToSet = value.asInt();
}
setMethod.invoke(object, valueToSet);
}
return object;
} catch (Exception e) {
throw new TokenizationException(GatewayConstants.STATUS_SYSTEM_ERROR,
"Error in deserializeIt for " + cls.getSimpleName() + " caused by " + e.getMessage(), e);
}
}
private String convertFieldName(String fieldName) {
StringBuilder newFieldName = new StringBuilder();
int length = fieldName.length();
boolean capitalize = true; //first character should be capitalized
for(int i = 0; i < length; i++) {
char current = fieldName.charAt(i);
if(current == '_') {
//remove the underscore and capitalize the next character in line
capitalize = true;
} else if(capitalize) {
newFieldName.append(Character.toUpperCase(current));
capitalize = false;
} else {
newFieldName.append(current);
}
}
return newFieldName.toString();
}
}
But I still need to create a new class per Request in order to pass in the proper class to create:
public class MyRequestDeserializer extends GenericRequestDeserializer {
#Override
public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
return deserializeIt(jsonParser, MyRequest.class);
}
}
Is there any way to get rid of all the MyRequestDeserializer classes? In other words, can the GenericRequestDeserializer figure out what class it is actually deserializing?
So I found a much better option for changing all my objects to snake case. Instead of using Serializers and Deserializers on each class, I was able to inject an ObjectMapper into the JsonProvider in Spring. ObjectMapper already supports a property that will do the camel-case to snake-case automagically. I just needed to overwrite the getSingletons method in my class that extends Application like so:
public class MyApp extends Application {
....
#Override
public Set<Object> getSingletons() {
final Set<Object> objects = new LinkedHashSet<Object>();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setPropertyNamingStrategy(
PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objects.add(new JacksonJsonProvider(objectMapper));
return objects;
}
}

How to Deserialize null to Double.NaN (Java, Jackson)

Question: The Jackson ObjectMapper deserializer is converting a null value to a 0 for a Double field. I need it to either be deserialized to null or Double.NaN. How can I do this?
Do I need to write a custom Double deserializer that maps null to Double.NaN?
Already tried: I have scoured the DeserializationFeature Enum but I don't think anything applies. (http://fasterxml.github.io/jackson-databind/javadoc/2.0.0/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_NULL_FOR_PRIMITIVES)
Motivation: I am deserializing a json object into a custom object (Thing) with code similar to the following. I need the deserializer to keep the value as null or change it to Double.NaN because I need to be able to differential between the 0 case (located at latitude/longitude/altitude = 0) and the null/Double.NaN case (when these values are unavailable).
Jackson deserializing
try {
ObjectMapper mapper = new ObjectMapper();
Thing t = mapper.readValue(new File("foobar/json.txt"), Thing.class);
} catch (JsonParseException e) {
...do stuff..
}
Contents of json.txt. Note that the value null is actually written in the file. It is not left empty. It is not the empty string. It is actuall the word null.
{
"thing" : {
"longitude" : null,
"latitude" : null,
"altitude" : null
}
}
Code for Thing
import java.io.Serializable;
public class Thing implements Serializable {
private static final long serialVersionUID = 1L;
Double latitude;
Double longitude;
Double altitude;
public Thing(Double latitude, Double longitude, Double altitude) {
this.latitude = latitude;
this.longitude = longitude;
this.altitude = altitude;
}
...rest of code...
}
This is what I did:
public class DoubleDeserializer extends JsonDeserializer<Double> {
#Override
public Double deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
String doubleStr = parser.getText();
if (doubleStr.isEmpty() || doubleStr == null) {
return null;
}
return new Double(doubleStr);
}
}
and then in my bean:
#JsonDeserialize(using = DoubleDeserializer.class)
private Double partialPressureCO2;
Hope this helps.
The solution that worked for me was to make a custom JSON Deserializer that transformed null into Double.NaN. Adapting what I wrote to match my example code above it would look like this.
public class ThingDeserializer extends JsonDeserializer<Thing> {
#Override
public Thing deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
Thing thingy = new Thing();
JsonNode node = jp.getCodec().readTree(jp);
if (node.get("latitude").isNull()) {
thingy.setLatitude(Double.NaN);
} else {
thingy.setLatitude(node.get("latitude").asDouble());
}
if (node.get("longitude").isNull()) {
thingy.setLongitude(Double.NaN);
} else {
thingy.setLongitude(node.get("longitude").asDouble());
}
if (node.get("altitude").isNull()) {
thingy.setAltitude(Double.NaN);
} else {
thingy.setLatitude(node.get("altitude").asDouble());
}
return thingy;
}
then I registered the deserializer in the class Thing by adding the annotation above the class declaration.
#JsonDeserialize(using = ThingDeserializer.class)
public class Thing implements Serializable {
... class code here ...
}
Note I think a better answer would be to deserialize the Double class rather than the Thing class. By deserializing the Double you could generalize the conversion from null to NaN. This would also do away with pulling the specific fields from the node by field name. I could not figure out how to do it on a limited time budget so this is what worked for me. Also, the deserialization is actually being implicitly called by Jackson through a REST api so I am not sure how/if this changes things. I would love to see a solution that would accomplish this though.

NumberFormatException in GSON when converting String to double

I am working with a JSON response that is improperly formatted. All fields are being returned as Strings. Unfortunately, I have no control over the return data.
I am using Gson and attempting to parse a JSON object that includes a field like this:
{
[...]
"cost": "9.25"
}
It should obviously be printed as a Number. When I try to parse this as a String, Number or double I get a NumberFormatException:
com.google.gson.JsonSyntaxException: java.lang.NumberFormatException:
[...]
at com.myapp.android.LauncherActivity$1.onSuccess(LauncherActivity.java:69)
[...]
Caused by: java.lang.NumberFormatException:
at org.apache.harmony.luni.util.FloatingPointParser.parseDouble(FloatingPointParser.java:267)
at java.lang.Double.parseDouble(Double.java:285)
at com.google.gson.stream.JsonReader.nextInt(JsonReader.java:599)
at com.google.gson.internal.bind.TypeAdapters$7.read(TypeAdapters.java:228)
... 19 more
LauncherActivity Line 69:
Item item = gson.fromJson(response, Item.class);
So I followed this similar question and tried creating a TypeAdapter like so:
public class CostTypeAdapter implements JsonDeserializer<Double>, JsonSerializer<Double> {
public Double deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Double cost;
try {
cost = json.getAsDouble();
} catch (NumberFormatException e) {
cost = 0.00d;
}
return cost;
}
public JsonElement serialize(Double src, Type typeOfSrc,
JsonSerializationContext context) {
return new JsonPrimitive(src);
}
}
And registered it when creating the GsonBuilder:
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Cost.class, new CostTypeAdapter());
Gson gson = builder.create();
And my Cost class:
public class Cost {
private Double value;
public Cost(Double value) {
this.value = value;
}
public Double getValue() {
return value;
}
}
But I get the same NumberFormatException.
Any ideas on whats happening here? Shouldn't this exception be caught in my CostTypeAdapter.deserialize(), at the very least?
Any help/guidance is greatly appreciated.
You can also use GsonBuilder's registerTypeAdapter() to catch possible parsing Exceptions and deal with them the way you want.
Example for catching NumberFormatException when parsing Float and make the value null:
GsonBuilder gb = new GsonBuilder();
gb.registerTypeAdapter(Float.class, new TypeAdapter<Float>() {
#Override
public Float read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
}
String stringValue = reader.nextString();
try {
Float value = Float.valueOf(stringValue);
return value;
} catch (NumberFormatException e) {
return null;
}
}
#Override
public void write(JsonWriter writer, Float value) throws IOException {
if (value == null) {
writer.nullValue();
return;
}
writer.value(value);
}
});
I ended up having to write a JsonDeserializer for my entire enclosing "Item" class.
public class ItemDeserializer implements JsonDeserializer<Item> {
#Override
public Item deserialize(JsonElement json, Type type,
JsonDeserializationContext context) throws JsonParseException {
JsonObject jobject = (JsonObject) json;
return new Item(
[...],
(jobject.has("cost")) ? jobject.get("cost").getAsDouble() : 0.00d
);
}
}
Would still love to see a solution for my original issue, so I wouldn't have to manually parse every field.
It looks like the cost field in your Item class is declared as an int, not as a double. Changing cost to a double should fix it.
Paul, I have a similar issue of getting numbers in my JSON that are stored as Strings. What's been working for me is this:
public enum Plan {
GUEST_PASS, FREE, PREMIUM;
static Plan fromValue(int value) {
for (Plan plan : plans)
if (value == plan.ordinal())
return plan;
throw new IllegalArgumentException("Invalid value for Plan: " + value);
}
static Plan fromValue(String string) {
try {
return fromValue(parseInt(string));
} catch (IllegalArgumentException _) {
throw new IllegalArgumentException("Invalid value for Plan: " + string);
}
}
private static EnumSet<Plan> plans = EnumSet.allOf(Plan.class);
}
public static class PlanAdapter extends TypeAdapter<Plan> {
#Override public void write(JsonWriter json, Plan plan) throws IOException {
json.value(Integer.toString(plan.ordinal()));
}
#Override public Plan read(JsonReader json) throws IOException {
return Plan.fromValue(json.nextString());
}
}
Seems like you have continuous data in your case, so you would suffice to have a class, I converted to an enum since I had discrete data.
I also ran into a similar situation. I used below adaptor for the conversion. I Found it concise.
.registerTypeAdapter(Double.class, new JsonSerializer<Double>() {
public JsonElement serialize(Double number, Type type, JsonSerializationContext context) {
return new JsonPrimitive(Double.valueOf(number));
}
})
the easiest solution is to make the attribute to be String instead of double and in the get method parse it to double.
for example:
class Location {
private String latitude;
private String longitude;
public double getLatitude() {
return latitude.isEmpty() ? 0d : Double.parseDouble(latitude);
}
public double getLongitude() {
return longitude.isEmpty() ? 0d : Double.parseDouble(longitude);
}
}

Custom Jackson Deserializer Getting Access to Current Field Class

I'm trying to write a custom deserializer for Jackson and I want to make it generic (generic in the sense of working on any type, not as in "generics").
However I cannot seem to figure out how to get a handle to the type of the field being deserialized.
Eg, I'm looking to do something like the following:
#Override
public MyObject deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Class c = <get type of current field>
// do something with that type
return new SubclassOfC(somedata based on c);
}
It's specifically the get type of current field part that I have struggled with.
Edit: It is the type of the java field I am interested in.
You don't -- deserializers are registered by type, so you need to construct deserializer to know what type it is expected to deserialize.
If you do want to registered a generic deserializer, you can however make things more dynamic by implementing ContextualDeserializer. Its createContextual() method is called with BeanProperty argument, and you can check things like name of the property (which may be null, in case of root values which are not referenced by a property) and type (which is the declared type).
This method can then return a new instance (do NOT modify original deserializer, since it is shared by all properties), configured with all extra information you need.
I have solved my particular problem by adding an implementation of Deserializers to the ObjectMapper. Eg
Deserializers d = new Deserializers.Base() {
#Override
public JsonDeserializer<?> findEnumDeserializer(Class<?> type, DeserializationConfig config, BeanDescription beanDesc, BeanProperty property)
throws JsonMappingException {
if (property.getType().getContentType() != null)
return new EnumDeserializer(property.getType().getContentType().getRawClass());
return new EnumDeserializer(property.getType().getRawClass());
}
};
mapper.setDeserializerProvider(mapper.getDeserializerProvider().withAdditionalDeserializers(d));
This will return my custom EnumDeserializer instantiated for each separate Enum type.
I solved it like this.
Get current field java type...
#Override
public Enum deserialize(JsonParser jsonparser, DeserializationContext context) throws IOException, JsonProcessingException {
System.out.println("EnumDeserializer ....");
Field field = findField(jsonparser.getCurrentName(), jsonparser.getCurrentValue().getClass());
Class<?> javaType = field.getType();
return null;
}
public Field findField(String name, Class<?> c) {
for (; c != null; c = c.getSuperclass()) {
for (Field field : c.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
if (field.getName().equals(name)) {
return field;
}
}
}
return null;
}
Roughly speaking, and without exception catching and error checking...
JsonToken tok = jp.nextValue();
Field field = findField(jp.getCurrentName());
Class<?> fc = field.getType();
if(fc == int.class) {
field.setInt(this, jp.getIntValue());
} // handle all the primitive types and String in the same way, then...
} ... else if(tok == JsonToken.START_ARRAY) {
if(fc.isArray()) {
// Load into an array
} else if(Collection.class.isAssignableFrom(fc)) {
// Load into a collection
} else {
// throw
}
} else if(tok == JsonToken.START_OBJECT) {
// Recursively create that object from the JSON stream
}
... and loop until tok is END_OBJECT. To find a of the current class by name:
Field findField(String name) {
for(Class<?> c = getClass(); c != null; c = c.getSuperclass()) {
for(Field field : c.getDeclaredFields()) {
if(field.getName().equals(name)) {
return field;
}
}
}
}

Categories