I'm having a map that I need to serialize but when I serialize it I get jackson infinite recursion (stackoverflowerror). So, after debugging a while I found the entry that caused this problem but this entry is a list which contains other objects that have some cyclic dependencies.
Let me write a sample code here.
Map<String, ?> params = new HashMap();
// Create some list and add objects with cyclic dependencies to it.
List<ContentObject> problematicList = new ArrayList();
//list.addAll(some problematic objects);
params.put("contentsModified", problematicList);
objectmapper.writeValueAsString(params); // here I get jackson infinite recursion (stackoverflowerror)
I tried marking the classes of the objects I found in the problematicList using #JsonIgnoreType but it didn't work. Is there any way to write some custom serializer to see if a map contains an entry with the name contentsModified and if it finds such entry it doesn't serialize the problematic lists?
You should fix the actual bi-directional mapping with problematic classes when serializing. For example if you have classes ContentObject & BackReferringObject referring to each other you can use #JsonBackReference like:
public class ContentObject {
private String name = "contentsModified";
private BackReferringObject backReferringObject;
}
public class BackReferringObject {
#JsonBackReference
private ContentObject contentObject;
}
This tells Jackson to not serialize contentObject back again.
If this is not possible you can always write some custom serializer. That would make things more complicated. For example, you could have something like this:
public class ContentObjectSerializer extends JsonSerializer<ContentObject> {
private final ObjectMapper objectMapper = new ObjectMapper();
#Override
public void serialize(ContentObject value, JsonGenerator gen,
SerializerProvider serializers)
throws IOException {
if (!value.getName().equals("contentsModified")) {
var sw = new StringWriter();
objectMapper.writeValue(sw, value);
gen.writeRawValue(sw.toString());
}
}
}
and usage:
var objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(ContentObject.class, new ContentObjectSerializer());
objectMapper.registerModule(module);
The latter configuration is really needed. You cannot for example annotate your ContentObject with #JsonSerialize(using = ContentObjectSerializer.class) since then the ObjectMapper inside custom serializer would cause another recursive call chain to custom serializer and cause stack overflow again.
I realize this is not directly applicable to a Map but as an example
Related
I am using JsonDeserializer to deserialize my custom Object, but in my method annotated with #KafkaListener get the object with Map field as null.
public ConsumerFactory<String, BizWebKafkaTopicMessage> consumerFactory(String groupId) {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new JsonDeserializer<>(BizWebKafkaTopicMessage.class));
}
and my BizWebKafkaTopicMessage is
#Data
public class BizWebKafkaTopicMessage {
// Elastic Search Index Name
private String indexName;
// ElasticSearch Index's type name
private String indexType;
// Source document to be used
private Map<String, Object> source; <=== This is being delivered as null.
// ElasticSearch document primary id
private Long id;
}
and the listener method listenToKafkaMessages
#KafkaListener(topics = "${biz-web.kafka.message.topic.name}", groupId = "${biz-web.kafka.message.group.id}")
public void listenToKafkaMessages(BizWebKafkaTopicMessage message) {
............................................
............................................
// Here message.source is null
............................................
............................................
}
Inside listenToKafkaMessages method, message argument looks like this
message.indexName = 'neeraj';
message.indexType = 'jain';
message.id = 123;
message.source = null;
My strong suspicion would be that it is the polymorphic nature of your value rather than Maps per-se.
Spring is using Jackson underneath the hood for it's serialisation/deserialisation - and by default Jackson (in serialisation) when handling Object instances does not encode what class it is serialising.
Why? Well it makes for bad compatibility issues e.g. you serialised an Object (Really MyPojoV1.class) into the database 1 year ago, and then later read it out - but your code has no MyPojoV1.class anymore because things have moved on... It can even cause issues if you move MyPojoV1 to a different package anywhere within the lifetime of your application!
So when it comes to deserialising Jackson doesn't know what class to deserialise the Object into.
A hacky idea would be to run the following somewhere:
ObjectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
Or the nicer/more spring way would be to:
#Configuration
public class JacksonConfiguration {
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
#Your Configuration Here for example mapper.configure(DeserializationFeature.something, true);
return mapper;
}
}
Finally it's worth adding that deserialising classes arbitrarily is generally a big security risk. There exists classes in Java which execute command line or even reflection based logic based on the value in their fields (which Jackson will happily fill for you). Hence someone can craft JSON such that you deserialise into a class that basically executes whatever command is in the value={} field.
You can read more about exploits here - although I recognise it may not concern you since your Kafka cluster and it's producers may inherently be within your 'trusted boundaries':
https://www.nccgroup.trust/globalassets/our-research/us/whitepapers/2018/jackson_deserialization.pdf
I have converted some info to Json format using Jackson in Java. Below is the output I get
[{"lat":45.9,"lon":10.9,"title":"Title A1","html":"<h3>Content A1</h3>","icon":"http://maps.google.com/mapfiles/markerA.png"},{"lat":44.8,"lon":1.7,"title":"Title B1","html":"<h3>Content B1</h3>","icon":"http://maps.google.com/mapfiles/markerB.png","show_infowindow":false},{"lat":51.5,"lon":-1.1,"title":"Title C1","html":"<h3>Content C1</h3><p>Lorem Ipsum..</p>","zoom":8,"icon":"http://maps.google.com/mapfiles/markerC.png"}]
My question is how can I get it in the below format, basically adding the Json to a root node which called locations
{"locations":[{"lat":45.9,"lon":10.9,"title":"Title A1","html":"<h3>Content A1</h3>","icon":"http://maps.google.com/mapfiles/markerA.png"},{"lat":44.8,"lon":1.7,"title":"Title B1","html":"<h3>Content B1</h3>","icon":"http://maps.google.com/mapfiles/markerB.png","show_infowindow":false},{"lat":51.5,"lon":-1.1,"title":"Title C1","html":"<h3>Content C1</h3><p>Lorem Ipsum..</p>","zoom":8,"icon":"http://maps.google.com/mapfiles/markerC.png"}]}
You may wrap the array into a JSONObject like so
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = new HashMap<String, Object>();
String json = jsonArray.toString();
map.put("locations", json);
json = mapper.writeValueAsString(map);
you can achieve this by following changes.
Let's Assume, your JSON will be created based upon Bean.java class likewise,
[{"lat":45.9,"lon":10.9,"title":"Title A1","html":"<h3>Content A1</h3>","icon":"http://maps.google.com/mapfiles/markerA.png"},{"lat":44.8,"lon":1.7,"title":"Title B1","html":"<h3>Content B1</h3>","icon":"http://maps.google.com/mapfiles/markerB.png","show_infowindow":false},{"lat":51.5,"lon":-1.1,"title":"Title C1","html":"<h3>Content C1</h3><p>Lorem Ipsum..</p>","zoom":8,"icon":"http://maps.google.com/mapfiles/markerC.png"}]
Now, As per your new requirement, you want something likewise,
{"locations":[{"lat":45.9,"lon":10.9,"title":"Title A1","html":"<h3>Content A1</h3>","icon":"http://maps.google.com/mapfiles/markerA.png"},{"lat":44.8,"lon":1.7,"title":"Title B1","html":"<h3>Content B1</h3>","icon":"http://maps.google.com/mapfiles/markerB.png","show_infowindow":false},{"lat":51.5,"lon":-1.1,"title":"Title C1","html":"<h3>Content C1</h3><p>Lorem Ipsum..</p>","zoom":8,"icon":"http://maps.google.com/mapfiles/markerC.png"}]}
So, in this case you need to create one more class, let's say it's SuperBean.java then it should be likewise,
public class SuperBean {
private Bean [] locations;
public Bean[] getBean() {
return locations;
}
public void setBean(Bean[] locations) {
this.locations = locations;
}
}
So, your JSON will be created likewise,
{"locations":[......]} // as per your requirement.
Let's say I have a POJO with quite a few fields. I also have a map with a bunch of properties that would map nicely to fields in the POJO. Now I want to apply the properties in the map to my POJO. How can I do this?
Jackson provides method new ObjectMapper().convertValue(), but that creates a fresh instance of the POJO. Do I really have to do something like this?
om = new ObjectMapper();
pojoMap = om.convertValue(pojo, Map.class);
pojoMap.putAll(properties);
pojo = om.convertValue(pojoMap, Pojo.class);
Isn't there an easier way?
As I have no experience with GSON and we also have it lying around here, how would I do that with GSON?
Yes, you can create an ObjectReader that will update an existing instance from the root JSON object rather than instantiating a new one, using the readerForUpdating method of ObjectMapper:
#Test
public void apply_json_to_existing_object() throws Exception {
ExampleRecord record = new ExampleRecord();
ObjectReader reader = mapper.readerForUpdating(record)
.with(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
.with(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES);
reader.readValue("{ firstProperty: 'foo' }");
reader.readValue("{ secondProperty: 'bar' }");
assertThat(record.firstProperty, equalTo("foo"));
assertThat(record.secondProperty, equalTo("bar"));
}
public static class ExampleRecord {
public String firstProperty;
public String secondProperty;
}
You can also create a value-updating reader from an existing ObjectReader. The following declaration seems equivalent:
ObjectReader reader = mapper.reader(ExampleRecord.class)
.withValueToUpdate(record)
.with(/* features etc */);
Addition
The above didn't actually answer your question, though.
Since you don't have the changes you want to make to the record as JSON, but rather as a map, you have to finagle things so that Jackson will read your Map. Which you can't do directly, but you can write the "JSON" out to a token buffer and then read it back:
#Test
public void apply_map_to_existing_object_via_json() throws Exception {
ExampleRecord record = new ExampleRecord();
Map<String, Object> properties = ImmutableMap.of("firstProperty", "foo", "secondProperty", "bar");
TokenBuffer buffer = new TokenBuffer(mapper, false);
mapper.writeValue(buffer, properties);
mapper.readerForUpdating(record).readValue(buffer.asParser());
assertThat(record.firstProperty, equalTo("foo"));
assertThat(record.secondProperty, equalTo("bar"));
}
(btw if this seems laborious, serializing to a token buffer and deserializing again is in fact how ObjectMapper.convertValue is implemented, so it's not a big change in functionality)
I have a map as below:
Map map = new HashMap<String, String>();
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
I need to convert this map into a JSON string. I know that this can be done using Jackson as below:
new ObjectMapper().writeValueAsString(map);
The problem is, I do not want to map key3 to the String. The output String should be as below:
{"key1":"value1","key2":"value2"}
Is there any way to avoid certain fields from the HashMap while serializing it to a String? Moreover, I want to add certain fields to the String while serializing. For instance, I need to add a field called "key4" with a value "value4". Thus, the final String should be:
{"key1":"value1","key2":"value2","key4":"value4"}
How do I do this using Jackson? Or is there any other way to do this in Java?
If you want to serialize a HashMap like this, you should implement a custom serializer. Here is one example:
public class CustomSerializer extends StdSerializer<Map> {
protected CustomSerializer() {
super(Map.class);
}
#Override
public void serialize(Map map, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
jsonGenerator.writeStartObject();
for (Object key : map.keySet()) {
if(!"key3".equals(key)){
jsonGenerator.writeStringField((String) key, (String) map.get(key));
}
}
jsonGenerator.writeStringField("key4","value4");
jsonGenerator.writeEndObject();
}
}
public class Main {
public static void main(String[] args) throws Exception{
Map<String, String> map = new HashMap<>();
map.put("key1","value1");
map.put("key2","value2");
map.put("key3","value3");
ObjectMapper mapper = new ObjectMapper();
SimpleModule serializerModule = new SimpleModule("SerializerModule", new Version(1, 0, 0, null, "mt", "customSerializerTest"));
serializerModule.addSerializer(new CustomSerializer());
mapper.registerModule(serializerModule);
System.out.println(mapper.writeValueAsString(map));
}
}
Output:
{"key1":"value1","key2":"value2","key4":"value4"}
If you are serializing a POJO, you can use the #JsonIgnore annotation, but in this case, this is not an option, so you should make a copy of the HashMap, and do whatever operations on it, what you need.
The simplest way really is to modify the Map.
However, if you want, you can actually use #JsonIgnoreProperties on Map type as well. So, something like:
#JsonIgnoreProperties({ "key3" })
public class MyFilteringMap extends HashMap<String,String> { }
It is not possible to add properties via annotations.
But it is possible to use #JsonAnySetter and #JsonAnyGetter annotations to make a POJO look like a Map. This would allow combining of approaches for filtering, as well as some "standard" properties and other misc properties. See this -- http://www.cowtowncoder.com/blog/archives/2011/07/entry_458.html -- for details.
You can try Jackson Custom Serializers - http://wiki.fasterxml.com/JacksonHowToCustomSerializers. I think it's convenient option for your use case. Using own serializer you can make transparent object processing in serialize method - for example you can copy map to another avoiding certain fields etc.
I have a slightly odd question. I have created an object, let's call it a Profile, that successfully parses single JSON objects via an API that I call. There is also a multi-profile interface that will return a JSON array of Profile objects. The problem is, the multi-profile interface turns the sub objects into strings. Is there an automatic way I can tell jackson to parse these into objects?
Example of a single object:
{ "foo": "bar" }
Example of a multi object:
[ "{ \"foo\": \"bar\" }", "{ \"blah\": \"ugh\" }" ]
(Sorry can't use real data)
Notice that the sub objects are actually strings, with escaped quotes inside them.
For completeness, my code for the multi object parse looks like this:
ObjectMapper mapper = new ObjectMapper();
Profile[] profile_array = mapper.readValue(response.content, Profile[].class);
for (Profile p: profile_array)
{
String user = p.did;
profiles.put(user, p);
}
As I said, in the single-profile case, the Profile object parses. In the multi-profile case, I get this exception:
Exception: org.codehaus.jackson.map.JsonMappingException: Can not construct instance of com.xyz.id.profile.Profile, problem: no suitable creator method found to deserialize from JSON String
I suppose you'll have to create a custom deserializer and apply it to the every element of that array.
class MyCustomDeserializer extends JsonDeserializer<Profile> {
private static ObjectMapper om = new ObjectMapper();
#Override
public Profile deserialize(JsonParser jp, DeserializationContext ctxt) {
// this method is responsible for changing a single text node:
// "{ \"foo\": \"bar\" }"
// Into a Profile object
return om.readValue(jp.getText(), Profile.class);
}
}
There is no out-of-the-box support for "re-parsing" of embedded JSON-in-JSON content.
But this sounds like a possible request for enhancement (RFE)...
Have you tried using JAXB?
final ObjectMapper mapper = new ObjectMapper();
// Setting up support of JAXB
final AnnotationIntrospector introspector = new JaxbAnnotationIntrospector();
// make deserializer use JAXB annotations (only)
mapper.getDeserializationConfig().setAnnotationIntrospector(
introspector);
// make serializer use JAXB annotations (only)
mapper.getSerializationConfig().setAnnotationIntrospector(
introspector);
final StringReader stringReader = new StringReader(response);
respGetClasses = mapper.readValue(stringReader,
FooBarClass.class);
The above should get you started...
Also, you would need to mark each subclass like so:
#XmlElement(name = "event")
public List<Event> getEvents()
{
return this.events;
}