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.
Related
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 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 + '}'; }
}
}
So I've been trying to get Jackson to serialize/deserialize one object I have which is essentially of the structure:
MyObject {
String a;
Map<String, Object> map;
}
where map can hold nested maps or 'primitive' values (String, Integer, Long, Double, ...)
Obviously, as the type information is required to correctly deserialize this I've had to tell Jackson to do this - for which I've used default typing:
return new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT, JsonTypeInfo.As.WRAPPER_ARRAY);
The object ends up serializing exactly how I'd like it to, adding type information only where it is necessary:
{
"a":"SomeString",
"map":{
"String1":"String1",
"Float1":[
"java.lang.Float",
1.0
],
"Long1":[
"java.lang.Long",
1
],
"Int1":1,
"Double1":1.0
}
}
However, when I attempt to deserialize this JSON with Jackson, it fails with the following error:
java.lang.IllegalArgumentException: Can not deserialize instance of <my type> out of START_ARRAY token
at [Source: N/A; line: -1, column: -1] (through reference chain: <my type>["name"])
at com.fasterxml.jackson.databind.ObjectMapper._convert(ObjectMapper.java:2615)
at com.fasterxml.jackson.databind.ObjectMapper.convertValue(ObjectMapper.java:2542)
at com.rbsgbm.agile.mongo.dbobject.JacksonDBObjectConverter.fromDBObject(JacksonDBObjectConverter.java:39)
at com.rbsgbm.agile.mongo.DBCursorIterator.next(DBCursorIterator.java:32)
at com.rbsgbm.agile.repository.StorageBasedRepository$StorageBasedQuery$StorageBasedQueryIterator.next(StorageBasedRepository.java:258)
at com.rbsgbm.agile.repository.StorageBasedRepository$StorageBasedQuery$StorageBasedQueryIterator.next(StorageBasedRepository.java:242)
at com.rbs.agile.strategy.strategymanager.store.mongo.MongoStrategyStore.loadStrategies(MongoStrategyStore.java:81)
This would suggest that it is getting confused about the value in the map where Jackson decided no type information is necessary, and thus didn't wrap it in an array.
Could somebody advise on the correct way to do this?
Hi there man please do this to solve your problem
first create a class MyObject with the appropriate getter and setter methods.
/**
* #author qualebs
*/
public class MyObject {
private String a;
private Map<String, Object> map;
public String getA() {
return a;
}
public void setA(String a) {
this.a = a;
}
public Map<String, Object> getMap() {
return map;
}
public void setMap(Map<String, Object> map) {
this.map = map;
}
}
then create an instance of your ObjectMapper with the following Configuration in the class where you are serializing MyObject.
ObjectMapper mapper = new ObjectMapper().configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// get an create a new MyObject
MyObject object = new MyObject();
// set the values you want ie the String a and the Map
Map<String, Object> map = new HashMap<String, Object>();
map.put("string", "example string");
map.put("int", 1);
map.put("long", 1l);
map.put("double", 2.0);
// we can also put an array
map.put("intArray", new int[]{1, 2, 3, 10});
// add the map to your object
object.setMap(map);
// set the string a
object.setA("example String 2");
// now we serialize the object
String mySerializedObj = mapper.writeValueAsString(object);
// to deserialize simply do
MyObject myUnserializedObj = mapper.readValue(mySerializedObj, MyObject.class);
please if this answers your questions accept my answer I could use the points. thank you.
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