I have a JSON that I convert into POJOs. The JSON is read from a GZIPInputStream gis.
ObjectMapper mapper = new ObjectMapper();
TypeReference<Map<Long, ConfigMasterAirportData>> typeRef =
new TypeReference<Map<Long, ConfigMasterAirportData>>() {};
Map<Long, ConfigMasterAirportData> configMasterAirportMap =
mapper.readValue(gis, typeRef);
I do not want new Long objects to be created for each entry. I want it to get Long objects from a custom LongPool I have created. Is there a way to pass such a LongPool to the mapper?
If not, is there another JSON library I can use to do that?
There are many ways to achieve this if you are sure that object pooling is required in your case.
First of all, Java already does Long object pooling for a small range between -128 and 127 inclusive. See source code of Long.valueOf.
Let us have 2 JSON objects that we want to deserialize: map1 and map2:
final String map1 = "{\"1\": \"Hello\", \"10000000\": \"world!\"}";
final String map2 = "{\"1\": \"You\", \"10000000\": \"rock!\"}";
Standard deserialization
If we use standard deserialization:
final ObjectMapper mapper = new ObjectMapper();
final TypeReference<Map<Long, String>> typeRef = new TypeReference<Map<Long, String>>() {};
final Map<Long, String> deserializedMap1 = mapper.readValue(map1, typeRef);
final Map<Long, String> deserializedMap2 = mapper.readValue(map2, typeRef);
printMap(deserializedMap1);
printMap(deserializedMap2);
Where printMap is defined as
private static void printMap(Map<Long, String> longStringMap) {
longStringMap.forEach((Long k, String v) -> {
System.out.printf("key object id %d \t %s -> %s %n", System.identityHashCode(k), k, v);
});
}
we get the following output:
key object id 1635756693 1 -> Hello
key object id 504527234 10000000 -> world!
key object id 1635756693 1 -> You
key object id 101478235 10000000 -> rock!
Note that key 1 is the same object with hashcode 1635756693 in both maps. This is due to built-in pool for [-128,127] range.
Solution1: #JsonAnySetter deserialization
We can define a wrapper object for the map and use #JsonAnySetter annotation to intercept all key-value pairs being deserialized. Then we can intern each Long object using Guava StrongInterner:
static class CustomLongPoolingMap {
private static final Interner<Long> LONG_POOL = Interners.newStrongInterner();
private final Map<Long, String> map = new HashMap<>();
#JsonAnySetter
public void addEntry(String key, String value) {
map.put(LONG_POOL.intern(Long.parseLong(key)), value);
}
public Map<Long, String> getMap() {
return map;
}
}
We will use it like this:
final ObjectMapper mapper = new ObjectMapper();
final Map<Long, String> deserializedMap1 = mapper.readValue(map1, CustomLongPoolingMap.class).getMap();
final Map<Long, String> deserializedMap2 = mapper.readValue(map2, CustomLongPoolingMap.class).getMap();
Output:
key object id 1635756693 1 -> Hello
key object id 1596467899 10000000 -> world!
key object id 1635756693 1 -> You
key object id 1596467899 10000000 -> rock!
Now you can see that key 10000000 is also the same object in both maps with hashcode 1596467899
Solution 2: Register custom KeyDeserializer
Define custom KeySerializer:
public static class MyCustomKeyDeserializer extends KeyDeserializer {
private static final Interner<Long> LONG_POOL = Interners.newStrongInterner();
#Override
public Long deserializeKey(String key, DeserializationContext ctxt) {
return LONG_POOL.intern(Long.parseLong(key));
}
}
And register it with the ObjectMapper:
final SimpleModule module = new SimpleModule();
module.addKeyDeserializer(Long.class, new MyCustomKeyDeserializer());
final ObjectMapper mapper = new ObjectMapper().registerModule(module);
final TypeReference<Map<Long, String>> typeRef = new TypeReference<Map<Long, String>>() {};
final Map<Long, String> deserializedMap1 = mapper.readValue(map1, typeRef);
final Map<Long, String> deserializedMap2 = mapper.readValue(map2, typeRef);
Solution 3: Use custom KeyDeserializer via #JsonDeserialize annotation
Define a wrapper object
static class MapWrapper {
#JsonDeserialize(keyUsing = MyCustomKeyDeserializer.class)
private Map<Long, String> map1;
#JsonDeserialize(keyUsing = MyCustomKeyDeserializer.class)
private Map<Long, String> map2;
}
And deserialize it:
final ObjectMapper mapper = new ObjectMapper();
final String json = "{\"map1\": " + map1 + ", \"map2\": " + map2 + "}";
final MapWrapper wrapper = mapper.readValue(json, MapWrapper.class);
final Map<Long, String> deserializedMap1 = wrapper.map1;
final Map<Long, String> deserializedMap2 = wrapper.map2;
Solution 4: Use Trove library TLongObjectMap to avoid using Long objects entirely
Trove library implements maps that use primitive types for keys to remove overhead of boxed objects entirely. It's in somewhat dormant state however.
You need in your case TLongObjectHashMap.
There is a library that defines a deserializer for TIntObjectMap:
https://bitbucket.org/marshallpierce/jackson-datatype-trove/src/d7386afab0eece6f34a0af69b76b478f417f0bd4/src/main/java/com/palominolabs/jackson/datatype/trove/deser/TIntObjectMapDeserializer.java?at=master&fileviewer=file-view-default
I think it will be quite easy to adapt it for TLongObjectMap.
Full code for this answer can be found here: https://gist.github.com/shtratos/f0a81515d19b858dafb71e86b62cb474
I've used answers to this question for solutions 2 & 3:
Deserializing non-string map keys with Jackson
Not sure about Jackson library, but with Google Gson you can quite simply do it by registering a custom type adapter whose responsibility is to resolve every key the way you want it:
public class DeserializeJsonMapWithCustomKeyResolver {
public static void main(String[] args) {
final String JSON = "{ \"1\" : { \"value\" :1 }, \"2\" : { \"value\" : 2} }";
final Type mapType = new TypeToken<Map<Long, ConfigMasterAirportData>>() {}.getType();
final Map<String, ConfigMasterAirportData> map =
new GsonBuilder().registerTypeAdapter(mapToken, new PooledLongKeyDeserializer())
.create()
.fromJson(JSON, mapType);
System.out.println(map);
}
static Long longFromString(String value)
{
System.out.println("Resolving value : " + value);
// TODO: replace with your LongPool call here instead; may need to convert from String
return Long.valueOf(value);
}
static class PooledLongKeyDeserializer implements
JsonDeserializer<Map<Long, ConfigMasterAirportData>>
{
#Override
public Map<Long, ConfigMasterAirportData> deserialize(
JsonElement json,
Type typeOfT,
JsonDeserializationContext context)
throws JsonParseException
{
final Map<Long, ConfigMasterAirportData> map = json.getAsJsonObject()
.entrySet()
.stream()
.collect(
Collectors.toMap(
e -> longFromString(e.getKey()),
e -> context.deserialize(e.getValue(),
TypeToken.get(ConfigMasterAirportData.class).getType())
));
return map;
}
}
static class ConfigMasterAirportData {
public int value;
#Override
public String toString() { return "ConfigMasterAirportData{value=" + value + '}'; }
}
}
Related
I want to convert POJO to MultiValueMap<String, String>(RequestParams).
POJO class:
public class Foo {
String fooStr;
int fooInt;
List<String> fooList;
}
Convertion code:
public static MultiValueMap<String, String> convert( Object obj ) {
MultiValueMap<String, String> requestParamMultiValueMap = new LinkedMultiValueMap<>();
Map<String, String> requestParamMap = new ObjectMapper()
.convertValue( obj, new TypeReference<Map<String, String>>() {} ); // Exception occurs here
requestParamMultiValueMap.setAll( requestParamMap );
return requestParamMultiValueMap;
}
When Foo class has only String member variables, it doesn't matter, but Foo class has List<String> member variable, and it causes
IllegalArgumentException:
Cannot deserialize value of type java.lang.String
from Array value (token JsonToken.START_ARRAY).
How to convert POJO with List<String> field to MultiValueMap using Jackson?
This can't be done automatically without customization, because the problem is too broad: each type needs to be converted to a list of strings, which is not obvious. Here we have some trivial cases (integer and string), but generally it would be good to control this conversion.
Here is a simple example where first we do a default conversion to Map<String, Object>, handled naturally by Jackson; then the custom if-else step follows, where each type goes through a special conversion. This step should be extended to fit the exact needs (and types) of the application. I guess proper test coverage would do the trick, because we can't rely 100% on static typing in this case.
It is important to notice that in case the Collection has other collections as elements the results depends on each particular collection "toString" method implementation, which probably could give unexpected results.
class TestJacksonConvert {
private static ObjectMapper OM = new ObjectMapper();
#Test
void test() {
var foo = new Foo();
foo.fooStr = "abc";
foo.fooInt = 21;
foo.fooList = List.of("a", "B", "c");
var converted = convert(foo);
assertEquals(
converted,
Map.of(
"fooStr", List.of("abc"),
"fooInt", List.of("21"),
"fooList", List.of("a", "B", "c")
)
);
}
public static LinkedMultiValueMap<String, String> convert(Object obj) {
var multiMap = new LinkedMultiValueMap<String, String>();
Map<String, Object> map = OM.convertValue(obj, new TypeReference<>() {});
map.forEach((key, value) -> {
if (value instanceof Collection<?> collection) {
multiMap.put(key, collection.stream().map(Object::toString).toList());
} else if (value != null) {
multiMap.put(key, List.of(value.toString()));
}
});
return multiMap;
}
}
This might a little slow but you can try this.
public MultiValueMap<String, String> convertToMultiValueMap(Object pojo) {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
Field[] fields = pojo.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String key = field.getName();
Object value = field.get(pojo);
if (value != null) {
map.add(key, value.toString());
}
}
return map;
}
This method uses reflection to get a list of all the fields in the POJO and adds each field to the map as a key-value pair.
Is it possible, when doing generic serialization, to add some intelligence in the ObjectMapper to rename some Json Fields in the following use case :
byte[] data
...
read(mapper.readValue(data, Map.class)
One rule could be something like " if the field contains the symbol '#', replace # by 'at_'" ?
EDIT 1:
To give details, let's say I have the following String :
String str = " {'name' : 'foo', '#somefield':'bar'}"
I use a ObjectMapper to translate it to a Map Object like defined above:
Map<String, String> map = mapper.readValue(str.bytes(), Map.class)
But I would like to know if it's possible to do some filtering / renaming during the readValue process of the ObjectMapper, for example removing the # symbol of the fields names.
The result would be a map with the following fields :
'name' : 'foo'
'somefield' : 'bar'
You can use StdDesiralizer.
Of course, you'll have to suffer with various options for the data structure (a nested list in one of the fields, numbers, etc.), but at least this is a working option.
P.S. You can use annotation to make code cleaner.
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module =
new SimpleModule("CustomJsonDeserializer", new Version(1, 0, 0, null, null, null));
module.addDeserializer(Map.class, new CustomJsonDeserializer());
mapper.registerModule(module);
String str = " {\"name\" : \"foo\", \"#somefield\":\"bar\"}";
Map<String, String> map = mapper.readValue(str.getBytes(), Map.class);
for (Map.Entry<String, String> e : map.entrySet()) {
System.out.printf("'%s' : '%s'\n", e.getKey(), e.getValue().toString());
}
}
public static class CustomJsonDeserializer extends StdDeserializer {
public CustomJsonDeserializer() {
this(null);
}
public CustomJsonDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Map<String, String> deserialize(JsonParser parser, DeserializationContext deserializer)
throws IOException {
Map<String, String> map = new HashMap<>();
ObjectCodec codec = parser.getCodec();
JsonNode nodes = codec.readTree(parser);
Iterator<Map.Entry<String, JsonNode>> iterator = nodes.fields();
while (iterator.hasNext()) {
Map.Entry<String, JsonNode> entry = iterator.next();
String key = entry.getKey();
if (key.contains("#")) {
key = key.replace("#", "at_");
}
map.put(key, entry.getValue().textValue());
}
return map;
}
}
Output:
'name' : 'foo'
'at_somefield' : 'bar'
When reading a JSON file, i would like to map my class as follows:
public class Effect {
private final String type;
private final Map<String, String> parameters;
public Effect(String type, Map<String, String> parameters) {
this.type = type;
this.parameters = parameters;
}
public String getType() {
return this.type;
}
public Map<String, String> getParameters() {
return this.parameters;
}
}
{
"type": {
"key1": "value1",
"key2": "value2",
}
}
So, the mapped JSON object consists of type as the only key and parameters as its value.
I would like to use #JsonCreator on the constructor, but can't figure out, how to map the fields. Do i need to write a custom deserializer or is there an easier way to map the class like i want?
I wrote a custom deserializer, which does what i want, but there might be an easier way, maybe with annotations alone, which i would like to know:
public class EffectDeserializer extends StdDeserializer<Effect> {
private static final long serialVersionUID = 1L;
public EffectDeserializer() {
super(Effect.class);
}
#Override
public Effect deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException {
JsonNode node = parser.getCodec().readTree(parser);
Iterator<String> fieldNames = node.fieldNames();
if(fieldNames.hasNext()) {
String type = fieldNames.next();
Map<String, String> parameters = new HashMap<>();
for(Iterator<Entry<String, JsonNode>> fields = node.get(type).fields(); fields.hasNext(); ) {
Entry<String, JsonNode> field = fields.next();
parameters.put(field.getKey(), field.getValue().textValue());
}
return new Effect(type, parameters);
}
return null;
}
}
Another way i found would be adding a JsonCreator (constructor in this case), that takes a Map.Entry<String, Map<String, String> and uses that to initialize the values, like this:
#JsonCreator
public Effect(Map.Entry<String, Map<String, String>> entry) {
this.type = entry.getKey();
this.parameters = entry.getValue();
}
If there's no way to get it done with a "normal" constructor, i will probably end up using this, as it uses Jackson's default mapping for Map.Entry, reducing possible error margin.
Add a static factory method that accepts a Map with a dynamic key:
#JsonCreator
public static Effect create(Map<String, Map<String, String>> map) {
String type = map.keySet().iterator().next();
return new Effect(type, map.get(type));
}
EDIT: Just noticed this is basically an uglier version of your own solution using Map.Entry. I would go with that instead.
I have a model that have a HashMap as you seen below:
private Map<String, String> attrMap = new HashMap<String, String>();
and initialize it like this:
attrMap.add("name", "value of name");
attrMap.add("content", "value of content");
but i want to serialize this field as a ArrayList of objects like this:
[{name: "value of name"}, {content: "value of content"}]
UPDATE 1
Is there a way to call a function during serialization like this:
#JsonSerializer(serializeAttrMap)
private Map<String, String> attrMap = new HashMap<String, String>();
public String serializeAttrMap() {
ArrayList<String> entries = new ArrayList<>(this.attrMap.size());
for(Map.Entry<String,String> entry : attrMap.entrySet())
entries.add(String.format("{%s: \"%s\"}",
entry.getKey(), entry.getValue()));
return Arrays.toString(entries.toArray());
}
UPDATE 2
I use this class to serialize attrMap, but get can not start an object expecting field name error.
import java.io.IOException;
import java.util.Map;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.SerializerProvider;
public class AttrMapSerializer extends JsonSerializer<Map<String, String>> {
#Override
public void serialize(Map<String, String> attributes, JsonGenerator generator, SerializerProvider provider) throws IOException, JsonProcessingException {
for (Map.Entry<String, String> attribute : attributes.entrySet())
{
generator.writeStartObject();
generator.writeObjectField("name", attribute.getKey());
generator.writeObjectField("content", attribute.getValue());
generator.writeEndObject();
}
}
}
I'm beginner to Jackson
The following construct will create the desired output:
#Test
public void testJackson() throws JsonProcessingException {
// Declare the map
Map<String, String> attrMap = new HashMap<>();
// Put the data in the map
attrMap.put("name", "value of name");
attrMap.put("content", "value of content");
// Use an object mapper
final ObjectMapper objectMapper = new ObjectMapper();
// Collect to a new object structure
final List<ObjectNode> collected = attrMap.entrySet()
.stream()
.map(entry -> objectMapper.createObjectNode().put(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
// The output
final String json = objectMapper.writeValueAsString(collected);
System.out.println(json); // -> [{"name":"value of name"},{"content":"value of content"}]
}
It uses a combination of the ObjectNode class from Jackson together with some Java 8 streams to collect the new data.
EDIT: After more info from the OP where they requested another approach I added this alternative.
Another approach is to simply use a #JacksonSerializer on the attribute.
// This is the serializer
public static class AttrMapSerializer extends JsonSerializer<Map<String, String>> {
#Override
public void serialize(
final Map<String, String> value,
final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
// Iterate the map entries and write them as fields
for (Map.Entry<String, String> entry : value.entrySet()) {
jgen.writeStartObject();
jgen.writeObjectField(entry.getKey(), entry.getValue());
jgen.writeEndObject();
}
}
}
// This could be the POJO
public static class PojoWithMap {
private Map<String, String> attrMap = new HashMap<>();
// This instructs the ObjectMapper to use the specified serializer
#JsonSerialize(using = AttrMapSerializer.class)
public Map<String, String> getAttributes() {
return attrMap;
}
}
public static void main(String... args) throws JsonProcessingException {
final PojoWithMap pojoWithMap = new PojoWithMap();
pojoWithMap.getAttributes().put("name", "value of name");
pojoWithMap.getAttributes().put("content", "value of content");
final String json = new ObjectMapper().writeValueAsString(pojoWithMap);
System.out.println(json); // ->
}
This way the serialization is externalized to a serializer and the POJO is intact.
Try with keySet and values method which returns set of keys and values and then convert to arraylist with something like:
List<String> keyList = new ArrayList<>(attrMap.keySet());
List<String> valueList = new ArrayList<>(attrMap.values());
To be very specific to your question, you need something like:
final Map<String, String> attrMap = new HashMap<String, String>();
attrMap.put("name", "value of name");
attrMap.put("content", "value of content");
List<String> keyList = new ArrayList<>(attrMap.size());
for (Map.Entry<String, String> entry : attrMap.entrySet()) {//iterate over map
keyList.add("{" + entry.getKey() + ": \"" + entry.getValue() + "\"}");//add key followed by value
}
System.out.println(keyList);
Output:
[{name: "value of name"}, {content: "value of content"}]
Note aside: It seems typo in your post as there is no add method in map. hope you meant put and not add. Also there are utilities like Gson, jackson etc avaiable to convert to json object.
If you don't mind doing it manually, then take the following code (not tested, but should work):
ArrayList<String> entries = new ArrayList<>(attrMap.size());
for(Map.Entry<String,String> entry : attrMap.entrySet())
entries.add(String.format("{%s: \"%s\"}",
entry.getKey(), entry.getValue()));
return Arrays.toString(entries.toArray());
This is probably the easiest way to do it, since if you want to use a JSON library, you have to either modify the output (not recommended since it imposes bad maintainability), or write a custom serializer/deserializer for HashMap, which will be more complicated.
I am doing the following:
IronRunId Id = new IronRunId("RunObject", "Runid1", 4);
ObjectMapper MAPPER = new ObjectMapper();
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("RunId", Id);
String json = MAPPER.writeValueAsString(map);
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map1 = mapper.readValue(json, new TypeReference<Map<String, Object>>() {});
IronRunId runId = (IronRunId) (map1.get("RunId"));
But this gives me an error: Cannot cast java.util.LinkedHashMap to IronRunId
Why is the object returned by map.get() of type linkedhashmap?
On the contrary, if I do:
List<Object> myList = new ArrayList<Object>();
myList.add("Jonh");
myList.add("Jack");
map.put("list", myList);
Then the object returned by map.get() after doing mapper.readValue is of type ArrayList.
Why the difference? Inserting default types into the map returns the correct object. But inserting custom made object in the map does not.
Does anyone know how to do this?
Map<String, Object> map1 = mapper.readValue(json, new TypeReference<Map<String, Object>>() {});
basically translated to, return me a Map with keys of type String and values of type Object. So, Jackson gave you keys of type String and values of type Object. Jackson doesn't know about your custom object, thats why it gave you its own native bound for Object which is a Map, specifically, a LinkedHashMap, and thus the reason why your are getting a LinkedHashMap when doing a get to the returned Map
So change it to :
Map<String, IronRunId> map1 = mapper.readValue(json, new TypeReference<Map<String, IronRunId>>() {});
Also, it is a good practice to declare an Object of its interface type than its concrete type. So instead of
HashMap<String, Object> map = new HashMap<String, Object>();
make it
Map<String, Object> map = new HashMap<String, Object>();
Edit
As a response to your added questions, you can create a wrapper object that will handle all your objects. Something like this.
class Wrapper{
private IronRunId ironRunId;
private long time;
private Map<String, String> aspects;
private String anotherString;
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public Map<String, String> getAspects() {
return aspects;
}
public void setAspects(Map<String, String> aspects) {
this.aspects = aspects;
}
public String getAnotherString() {
return anotherString;
}
public void setAnotherString(String anotherString) {
this.anotherString = anotherString;
}
public IronRunId getIronRunId() {
return ironRunId;
}
public void setIronRunId(IronRunId ironRunId) {
this.ironRunId = ironRunId;
}
}
You can then store different objects in this class.
Revised version
public static void main(String[] args) throws JsonGenerationException, JsonMappingException, IOException{
IronRunId Id = new IronRunId("RunObject", "Runid1", 4);
Map<String, String> aspects = new HashMap<String, String>();
aspects.put("aspectskey1", "aspectsValue1");
aspects.put("aspectskey2", "aspectsValue2");
aspects.put("aspectskey3", "aspectsValue3");
String anotherString = "anotherString";
long time = 1L;
Wrapper objectWrapper = new Wrapper();
ObjectMapper objectMapper = new ObjectMapper();
objectWrapper.setIronRunId(Id);
objectWrapper.setTime(time);
objectWrapper.setAnotherString(anotherString);
objectWrapper.setAspects(aspects);
Map<String, Wrapper> map = new HashMap<String, Wrapper>();
map.put("theWrapper", objectWrapper);
String json = objectMapper.writeValueAsString(map);
Map<String, Wrapper> map1 = objectMapper.readValue(json, new TypeReference<Map<String, Wrapper>>() {});
Wrapper wrapper = map1.get("theWrapper");
System.out.println("run id : " + wrapper.getIronRunId().toString());
System.out.println("time : " + wrapper.getTime());
System.out.println("aspects : " + wrapper.getAspects().toString());
System.out.println("anotherString : " + wrapper.getAnotherString());
}
new TypeReference<Map<String, Object>> is too generic. It is equivalent Map or "untyped" Map mentioned in Data Binding With Generics. The only way for you to deserialize different datatypes in a map or collection is to use TypeFactory.parametricType