JsonNode comparison excluding fields - java

I'm trying to deserialise a JSON string using ObjectMapper (Jackson) and exclude a field while performing the deserialisation.
My code is as follows:
String aContent = new String(Files.readAllBytes(Paths.get(aFile)));
String bContent = new String(Files.readAllBytes(Paths.get(bFile)));
ObjectMapper mapper = new ObjectMapper();
FilterProvider filterProvider = new SimpleFilterProvider()
.addFilter("_idFilter", SimpleBeanPropertyFilter.serializeAllExcept("_id"));
mapper.setFilterProvider(filterProvider);
JsonNode tree1 = mapper.readTree(aContent);
JsonNode tree2 = mapper.readTree(bContent);
String x = mapper.writeValueAsString(tree1);
return tree1.equals(tree2);
Both x and tree1 and tree2 contains the value _id in the JSON String but it isn't removed.

You are following Ignore Fields Using Filters except the first step
First, we need to define the filter on the java object:
#JsonFilter("myFilter")
public class MyDtoWithFilter { ... }
So currently you supposed to add
#JsonFilter("_idFilter")
public class JsonNode {
It's not possible so you need to create a class that extends JsonNode and use it instead
#JsonFilter("_idFilter")
public class MyJsonNode extends JsonNode {
If you don't want to implement all abstract method define as abstract
#JsonFilter("_idFilter")
public abstract class MyJsonNode extends JsonNode {
}
And in your code:
MyJsonNode tree1 = (MyJsonNode) mapper.readTree(aContent);
MyJsonNode tree2 = (MyJsonNode) mapper.readTree(bContent);

FilterProvider are meant to be used with custom Object like here
If you want to stick with JsonNode, use this method :
String aContent = new String("{\"a\":1,\"b\":2,\"_id\":\"id\"}");
ObjectMapper mapper = new ObjectMapper();
JsonNode tree1 = mapper.readTree(aContent);
ObjectNode object = (ObjectNode) tree1;
object.remove(Arrays.asList("_id"));
System.out.println(mapper.writeValueAsString(object));
Will print :
{"a":1,"b":2}

If you use Jackson 2.6 or higher, you can use a FilteringParserDelegate with a custom TokenFilter.
public class PropertyBasedIgnoreFilter extends TokenFilter {
protected Set<String> ignoreProperties;
public PropertyBasedIgnoreFilter(String... properties) {
ignoreProperties = new HashSet<>(Arrays.asList(properties));
}
#Override
public TokenFilter includeProperty(String name) {
if (ignoreProperties.contains(name)) {
return null;
}
return INCLUDE_ALL;
}
}
When creating the FilteringParserDelegate with this PropertyBasedIgnoreFilter, make sure to set the booleans includePath and allowMultipleMatches both to true.
public class Main {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
String jsonInput = "{\"_p1\":1,\"_p2\":2,\"_id\":\"id\",\"_p3\":{\"_p3.1\":3.1,\"_id\":\"id\"}}";
JsonParser filteredParser = new FilteringParserDelegate(mapper.getFactory().createParser(new ByteArrayInputStream(jsonInput.getBytes())),
new PropertyBasedIgnoreFilter("_id"),
true,
true);
JsonNode tree = mapper.readTree(filteredParser);
System.out.println(jsonInput);
System.out.println(tree);
System.out.println(jsonInput.equals(tree.toString()));
}
}
prints
{"_p1":1,"_p2":2,"_id":"id","_p3":{"_p3.1":3.1,"_id":"id"}}
{"_p1":1,"_p2":2,"_p3":{"_p3.1":3.1,"_id":"id"}}
false
As you can see, nested occurrences of _idare not filtered out. In case that's not what you need, you can of course extend my PropertyBasedIgnoreFilter with your own TokenFilter implementation.

Related

Jackson cannot deserialize empty byte array/stream

Can I somehow alter ObjectMapper to be able to handle null and empty values?
Let's say that my value is read as
objectMapper.readValue(val, new TypeReference<Object>() {});
Where val is
val = new ByteArrayInputStream(new byte[] {});
I don't have control over value that is passed and I cannot check for buffer length prior to executing readValue.
I've tried configuring mapper with DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT such as:
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true);
But I still get the com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input error. Is it possible to somehow have Jackson ignore empty values and just return null?
Is it possible to somehow have Jackson ignore empty values and just return null?
You can successfully dial with an empty byte array, or an empty input stream, by using a more low-level streaming API.
That's the core idea of how you can ensure that there's some to parse by employing a JsonParser before feeding the data into an ObjectMapper:
byte[] jsonBytes1 = {};
ObjectMapper mapper = new ObjectMapper();
JsonParser parser = mapper.getFactory().createParser(jsonBytes1);
JsonNode node = parser.readValueAsTree();
MyPojo myPojo = null;
if (node != null) {
myPojo = mapper.treeToValue(node, MyPojo.class);
}
So we're parsing the input into a JsonNode and checking it manually, only if it's not null ObjectMapper comes into play.
If we extract this logic into a separate method, that's it might look like (Java 8 Optional might be handy in this case a return type):
public static <T> Optional<T> convertBytes(byte[] arr,
Class<T> pojoClass,
ObjectMapper mapper) throws IOException {
JsonParser parser = mapper.getFactory().createParser(arr);
JsonNode node = parser.readValueAsTree();
return node != null ? Optional.of(mapper.treeToValue(node, pojoClass)) : Optional.empty();
}
Usage example
Consider a simple POJO:
public class MyPojo {
private String name;
// getter, setters and toString
}
main()
public static void main(String[] args) throws IOException {
String source = """
{
"name" : "Alice"
}
""";
byte[] jsonBytes1 = {};
byte[] jsonBytes2 = source.getBytes(StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper();
System.out.println(convertBytes(jsonBytes1, MyPojo.class, mapper));
System.out.println(convertBytes(jsonBytes2, MyPojo.class, mapper));
}
Output:
Optional.empty
Optional[Test.MyPojo(name=Alice)]

Convert enum to JSON String with Jackson

Im trying to convert an Enum class into a JSON string using jackson, the problem is the class is in a jar file so I am looking for better soultion then changing it.
when I use this code I get the following output:
Code
ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
BrainWave brainwave = BrainWave.DELTA;
brainwave.value(50);
System.out.println(ow.writeValueAsString(brainwave));
Output
"DELTA"
The output I want:
{
"type" : 1,
"value" : 50
}
I know i can use #JsonFormat but As I stated before, I rather not change the jar file.
Try a StdDeserializer - this article on Enum Serialization shows the different ways but in your case you'll want something like this (this is a rewrite of their example at the bottom based on your snippet above)
public class DeltaEnumDeserializer extends StdDeserializer<DELTA> {
#Override
public Distance deserialize(JsonParser jsonParser, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
final JsonNode node = jsonParser.getCodec().readTree(jsonParser);
final int type = node.get("type").asInt();
final int value = node.get("value").asInt();
for (final DELTA curr : DELTA.values()) {
if (curr.type == type && curr.value == value) {
return curr;
}
}
return null;
}
}
This article shows a snippet of code on linking up the deserializer. ie.
final ObjectMapper mapper = new ObjectMapper();
final SimpleModule module = new SimpleModule();
module.addDeserializer(DELTA.class, new DeltaEnumDeserializer());
mapper.registerModule(module);
DELTA readValue = mapper.readValue(json, DELTA.class);

Jackson ObjectMapper: How to omit (ignore) fields of certain type from serialization?

How can I tell Jackson ObjectMapper to ignore fields of certain type (class), in my case of Object.class, from serialization?
Constrains:
No control of source class - it is a third party class
Class type being serialized is unknown upfront - I guess it disqualifies MixIn(s)
Such field(s) name is unknown upfront
To help, below is a unit test expecting fields objectsList and objectField to be ignored from serialization, but its approach is not correct, it is filtering them by name instead of by their type.
public static class FavoriteShows {
public Simpsons favorite = new Simpsons();
public BigBangTheory preferred = new BigBangTheory();
}
public static class Simpsons {
public String title = "The Simpsons";
public List<Object> objectsList = List.of("homer", "simpson");
public Object objectField = new HashMap() {{
put("mr", "burns");
put("ned", "flanders");
}};
}
public static class BigBangTheory {
public String title = "The Big Bang Theory";
public List<Object> objectsList = List.of("sheldon", "cooper");
public Object objectField = new HashMap() {{
put("leonard", "hofstadter");
put("Raj", "koothrappali");
}};
}
public abstract static class MyMixIn {
#JsonIgnore
private Object objectField;
#JsonIgnore
private Object objectsList;
}
#Test
public void test() throws JsonProcessingException {
// GIVEN
// Right solution must work for any (MixIn(s) is out of questions) Jackson annotated class
// without its modification.
final ObjectMapper mapper = new ObjectMapper()
.addMixIn(Simpsons.class, MyMixIn.class)
.addMixIn(BigBangTheory.class, MyMixIn.class);
// WHEN
String actual = mapper.writeValueAsString(new FavoriteShows());
System.out.println(actual);
// THEN
// Expected: {"favorite":{"title":"The Simpsons"},"preferred":{"title":"The Big Bang Theory"}}
assertThat(actual).isEqualTo("{\"favorite\":{\"title\":\"The Simpsons\"},\"preferred\":{\"title\":\"The Big Bang Theory\"}}");
}
One of the way is to use custom AnnotationIntrospector.
class A {
int three = 3;
int four = 4;
B b = new B();
// setters + getters
}
class B {
int one = 1;
int two = 2;
// setters + getters
}
To ignore all fields with type B:
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
#Override
protected boolean _isIgnorable(Annotated a) {
return super._isIgnorable(a)
// Ignore B.class
|| a.getRawType() == B.class
// Ignore List<B>
|| a.getType() == TypeFactory.defaultInstance()
.constructCollectionLikeType(List.class, B.class);
}
});
String json = mapper.writeValueAsString(new A());
If you are using mixins you should be able to annotate with #JsonIgnoreType to have it ignore the class. docs For reference Globally ignore class in Jackson

Jackson JSON API Deserialization Streaming Unrecognized Field

I have this json:
[{"cdCondicaoPagto":"1","NrParcela":1,"NrDias":0}]
and this class:
public static class CondicaoPagtoItem implements Serializable {
private String cdCondicaoPagto;
private Integer NrParcela;
private Integer NrDias;
public CondicaoPagtoItem() {
}
public String getCdCondicaoPagto() {
return cdCondicaoPagto;
}
public void setCdCondicaoPagto(String cdCondicaoPagto) {
this.cdCondicaoPagto = cdCondicaoPagto;
}
public Integer getNrParcela() {
return NrParcela;
}
public void setNrParcela(Integer NrParcela) {
this.NrParcela = NrParcela;
}
public Integer getNrDias() {
return NrDias;
}
public void setNrDias(Integer NrDias) {
this.NrDias = NrDias;
}
}
And I'm trying to read it by streaming, this way:
JsonFactory jsonFactory = new JsonFactory();
ObjectMapper jsonMapper = new ObjectMapper(jsonFactory);
JsonNode jsonNodeGeral = jsonMapper.readTree(new File("/home/cechinel/Documentos/CondicaoPagtoItem.json"));
Iterator<JsonNode> elements = jsonNodeGeral.getElements();
while(elements.hasNext()){
JsonNode jsonNode = elements.next();
CondicaoPagtoItem condicao = jsonMapper.treeToValue(jsonNode, CondicaoPagtoItem.class);
}
But It causing the following error:
UnrecognizedPropertyException: Unrecognized field "NrParcela"
If I use the annotation #JsonProperty it works, but I don't want to do it in which integer field.
It sounds to me more like it's a naming convention mismatch. setNrParcela would map to a field name nrParcela but your JSON document has the 'n' capitalized as NrParcela.
If you cannot change the JSON field capitalization, you can use #JsonProperty with an overridden name:
#JsonProperty("NrParcela")
But since you didn't want to do that, another option to consider is implementing a PropertyNamingStrategy.

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;
}
}

Categories