I want to print HashMultiMap as json.
HashMultimap<String,Object> multimap = HashMultimap.create();
multimap.put("a",Obj1);
multimap.put("a",Obj3);
multimap.put("b",Obj2);
to
{
"a":[Obj1,Obj3],
"b":[Obj2]
}
Obj1 and other objects should again be in json(to keep it clean, I have shown it as objects)
I can iterate over the individual keys and convert set of Objects to json using libraries such as Gson.
But to get the entire snapshot of the HashMultimap, I want to convert it to json and inspect it.
Gson could not convert the entire map, but could do individual values(list of objects to json)
Call asMap() on the MultiMap first. This converts the MultiMap to a standard Map where each value is a Collection.
In your example, the type of the resulting Map is Map<String, Collection<Object>>. Gson should be able to serialise this correctly.
You need to write a JsonAdapter or both JsonDeserializer and JsonSerializer. It's rather terrible, but I wanted to try.
Basically, you delegate everything to a Map<String, Collection<V>>.
static class MultimapAdapter implements JsonDeserializer<Multimap<String, ?>>, JsonSerializer<Multimap<String, ?>> {
#Override public Multimap<String, ?> deserialize(JsonElement json, Type type,
JsonDeserializationContext context) throws JsonParseException {
final HashMultimap<String, Object> result = HashMultimap.create();
final Map<String, Collection<?>> map = context.deserialize(json, multimapTypeToMapType(type));
for (final Map.Entry<String, ?> e : map.entrySet()) {
final Collection<?> value = (Collection<?>) e.getValue();
result.putAll(e.getKey(), value);
}
return result;
}
#Override public JsonElement serialize(Multimap<String, ?> src, Type type, JsonSerializationContext context) {
final Map<?, ?> map = src.asMap();
return context.serialize(map);
}
private <V> Type multimapTypeToMapType(Type type) {
final Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
assert typeArguments.length == 2;
#SuppressWarnings("unchecked")
final TypeToken<Map<String, Collection<V>>> mapTypeToken = new TypeToken<Map<String, Collection<V>>>() {}
.where(new TypeParameter<V>() {}, (TypeToken<V>) TypeToken.of(typeArguments[1]));
return mapTypeToken.getType();
}
}
The full code including a test can be found here.
Related
In a REST controller in Spring boot, I am trying to iterate the values in a RequestBody response and put some of them in a HashMap in a POST endpoint.
The JSON I am sending is of this structure:
{"name":"yogurt","vitaminA":6,"vitaminb12":5}
The endpoint looks like this so far:
#RequestMapping("/create")
public NutrientList createNUtrientList(#RequestBody NutrientList nutrientList) {
Map<String, Double> nutrientMap = new HashMap<String,Double>();
//get nutrient values, need help with this part
for()
//add values to map
NutrientList nl = new NutrientList(nutrientList.getName(), nutrientMap);
//will save to repository
return nl;
}
The NutrientList class looks like this:
public class NutrientList {
#Id
private ObjectId id;
#JsonProperty("name")
private String name;
#JsonProperty("nutrientMap")
Map <String,Double> nutrientMap = new HashMap<String,Double>();
public NutrientList() {}
public NutrientList(String name, Map<String, Double> nutrientMap) {
this.id = new ObjectId();
this.name = name;
this.nutrientMap = nutrientMap;
}
//setters and getters
}
The data is stored by separate nutrient in the database, it is not a map. I see the NutrientList class does not share the same structure, but is there any way I can get around this to be able to use a map without changing how it is stored in the database?
I need to use a map because there are many nutrients and I don't want to have separate variables for them. Thank you so much. Let me know if something is not clear.
EDIT:
I could alternately turn the csv where I got the data in the database from into JSON format with the map, but I have not found a tool online that gives me this flexibility.
If you have a list of valid keys, you could use the following:
private static final List<String> validKeys = Arrays.asList("vitaminA", "vitaminB" /* ... */);
#RequestMapping("/create")
public NutrientList createNutrientList(#RequestBody Map<String, Object> requestBody) {
Map<String, Double> nutrientMap = new HashMap<>();
for (String nutrient : requestBody.keySet()) {
if (validKeys.contains(nutrient) && requestBody.get(nutrient) instanceof Number) {
Number number = (Number) requestBody.get(nutrient);
nutrientMap.put(nutrient, number.doubleValue());
}
}
String name = (String) requestBody.get("name"); // maybe check if name exists and is really a string
return new NutrientList(name, nutrientMap);
}
If you want to use Java 8 Stream API you can try:
private static final List<String> validKeys = Arrays.asList("vitaminA", "vitaminB" /* ... */);
#RequestMapping("/create")
public NutrientList createNutrientList(#RequestBody Map<String, Object> requestBody) {
Map<String, Double> nutrientMap = requestBody.entrySet().stream()
.filter(e -> validKeys.contains(e.getKey()))
.filter(e -> e.getValue() instanceof Number)
.collect(Collectors.toMap(Map.Entry::getKey, e -> ((Number) e.getValue()).doubleValue()));
String name = Optional.ofNullable(requestBody.get("name"))
.filter(n -> n instanceof String)
.map(n -> (String) n)
.orElseThrow(IllegalArgumentException::new);
return new NutrientList(name, nutrientMap);
}
Hope that helps.
Suppose I have an object with
private Double test;
// Need specific output in JSON via Jackson: test = 24.6000
When output to JSON via Jackson, I get 24.6, but I need the exact 4-decimal output as in the example. Does Jackson allow this?
For example, for Dates, we found a way to force MM/dd/yyyy:
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "MM/dd/yyyy")
Date myDate;
We need something similar for Decimal formatting.
One way of doing this is to use custom json serializer and specify in #JsonSerialize.
#JsonSerialize(using = CustomDoubleSerializer.class)
public Double getAmount()
public class CustomDoubleSerializer extends JsonSerializer<Double> {
#Override
public void serialize(Double value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
if (null == value) {
jgen.writeNull();
} else {
final String pattern = ".####";
final DecimalFormat myFormatter = new DecimalFormat(pattern);
final String output = myFormatter.format(value);
jgen.writeNumber(output);
}
}
}
You can try to use com.fasterxml.jackson.databind.util.RawValue:
BigDecimal d = new BigDecimal(new BigInteger("246000"), 4);
RawValue rv = new RawValue(d.toPlainString());
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode output = objectMapper.createObjectNode();
output.putRawValue("decimal_value", rv);
System.out.println(output.toPrettyString());
//output is:
//{
// "decimal_value" : 24.6000
//}
I have a JSON which sends array of element in normal cases but sends empty string "" tag without array [] brackets in case of 0 elements.
How to handle this with Gson? I want to ignore the error and not cause JSONParsingException.
eg.
"types": [
"Environment",
"Management",
"Computers"
],
sometimes it returns:
"types" : ""
Getting the following exception: Expected BEGIN ARRAY but was string
Since you don't have control over the input JSON string, you can test the content and decide what to do with it.
Here is an example of a working Java class:
import com.google.gson.Gson;
import java.util.ArrayList;
public class Test {
class Types {
Object types;
}
public void test(String input) {
Gson gson = new Gson();
Types types = gson.fromJson(input,Types.class);
if(types.types instanceof ArrayList) {
System.out.println("types is an ArrayList");
} else if (types.types instanceof String) {
System.out.println("types is an empty String");
}
}
public static void main(String[] args) {
String input = "{\"types\": [\n" +
" \"Environment\",\n" +
" \"Management\",\n" +
" \"Computers\"\n" +
" ]}";
String input2 = "{\"types\" : \"\"}";
Test testing = new Test();
testing.test(input2); //change input2 to input
}
}
If a bad JSON schema is not under your control, you can implement a specific type adapter that would try to determine whether the given JSON document is fine for you and, if possible, make some transformations. I would recomment to use #JsonAdapter in order to specify improperly designed types (at least I hope the entire API is not improperly designed).
For example,
final class Wrapper {
#JsonAdapter(LenientListTypeAdapterFactory.class)
final List<String> types = null;
}
where LenientListTypeAdapterFactory can be implemented as follows:
final class LenientListTypeAdapterFactory
implements TypeAdapterFactory {
// Gson can instantiate it itself, let it just do it
private LenientListTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Obtaining the original list type adapter
#SuppressWarnings("unchecked")
final TypeAdapter<List<?>> realListTypeAdapter = (TypeAdapter<List<?>>) gson.getAdapter(typeToken);
// And wrap it up in the lenient JSON type adapter
#SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) new LenientListTypeAdapter(realListTypeAdapter);
return castTypeAdapter;
}
private static final class LenientListTypeAdapter
extends TypeAdapter<List<?>> {
private final TypeAdapter<List<?>> realListTypeAdapter;
private LenientListTypeAdapter(final TypeAdapter<List<?>> realListTypeAdapter) {
this.realListTypeAdapter = realListTypeAdapter;
}
#Override
public void write(final JsonWriter out, final List<?> value)
throws IOException {
realListTypeAdapter.write(out, value);
}
#Override
public List<?> read(final JsonReader in)
throws IOException {
// Check the next (effectively current) JSON token
switch ( in.peek() ) {
// If it's either `[...` or `null` -- we're supposing it's a "normal" list
case BEGIN_ARRAY:
case NULL:
return realListTypeAdapter.read(in);
// Is it a string?
case STRING:
// Skip the value entirely
in.skipValue();
// And return a new array list.
// Note that you might return emptyList() but Gson uses mutable lists so we do either
return new ArrayList<>();
// Not anything known else?
case END_ARRAY:
case BEGIN_OBJECT:
case END_OBJECT:
case NAME:
case NUMBER:
case BOOLEAN:
case END_DOCUMENT:
// Something definitely unexpected
throw new MalformedJsonException("Cannot parse " + in);
default:
// This would never happen unless Gson adds a new type token
throw new AssertionError();
}
}
}
}
Here is it how it can be tested:
for ( final String name : ImmutableList.of("3-elements.json", "0-elements.json") ) {
try ( final Reader reader = getPackageResourceReader(Q43562427.class, name) ) {
final Wrapper wrapper = gson.fromJson(reader, Wrapper.class);
System.out.println(wrapper.types);
}
}
Output:
[Environment, Management, Computers]
[]
If the entire API uses "" for empty arrays, then you can drop the #JsonAdapter annotation and register the LenientListTypeAdapterFactory via GsonBuilder, but add the following lines to the create method in order not to break other type adapters:
if ( !List.class.isAssignableFrom(typeToken.getRawType()) ) {
// This tells Gson to try to pick up the next best-match type adapter
return null;
}
...
There are a lot of weirdly designed JSON response choices, but this one hits the top #1 issue where nulls or empties are represented with "". Good luck!
Thanks for all your answers.
The recommed way as mentioned in above answers would be to use TypeAdapters and ExclusionStrategy for GSON.
Here is a good example Custom GSON desrialization
I am the recipient of a webhook POST that looks like this, which I have decoded for readability.
id=12345&event=this_event&payload[customer][name]=ABC Company&payload[customer][city]=New York&payload[service][name]=New Customer&payload[service][action]=New
Using Spring MVC, I can easily get this into a Map<String, Sting> that looks like this
{id=97659204, event=test, payload[customer][name]=ABC Company, payload[customer][city]=New York, payload[service][name]=New Customer, payload[service][action]=New}
I need to parse every parameter (or Map key) that starts with "payload" into a JSON object.
My desired output from parsing the request parameters that start with "payload" would look something like this
{
customer : {
name : "ABC Company",
city : "New York"
},
service : {
name : "New Customer",
action : "New"
}
}
With the final state being a call to Jackson's ObjectMapper to turn that JSON into a POJO.
Since I have no control over the data format begin sent to me, what is the best/correct option for parsing those request parameters into a JSON object?
Thanks.
I ended up writing a custom parser for the payload[][][] parameters being passed in. It uses regex matcher and then recursion to analyze each parameter, traverse a Map of 1...n Maps and then the resulting Map makes perfect JSON.
#RequestMapping(value = "/receiver", method = RequestMethod.POST)
#ResponseBody
public void receiver(#RequestParam Map<String, Object> requestBody) throws IOException {
Pattern pattern = Pattern.compile("\\[([^\\]]+)]");
Map<String, Object> payloadMap = new HashMap<>();
Matcher matcher = null;
List<String> levels = null;
for (String key : requestBody.keySet()) {
if (key.startsWith("payload")) {
matcher = pattern.matcher(key);
levels = new ArrayList<>();
while (matcher.find()) {
levels.add(matcher.group(1));
}
payloadMap = nestMap(payloadMap, levels.iterator(), (String) requestBody.get(key));
}
}
LOG.debug(mapper.writeValueAsString(payloadMap));
}
#SuppressWarnings("unchecked")
private Map<String, Object> nestMap(Map<String, Object> baseMap, Iterator<String> levels, String value) {
String key = levels.next();
if (!levels.hasNext()) {
baseMap.put(key, value);
} else {
if (!baseMap.containsKey(key)) {
baseMap.put(key, nestMap(new HashMap<String, Object>(), levels, value));
} else {
baseMap.put(key, nestMap((Map<String, Object>) baseMap.get(key), levels, value));
}
}
return baseMap;
}
I am getting a a duplicate key exception while parsing JSON response containing timestamps as keys using GSON. It gives the following error:
com.google.gson.JsonSyntaxException: duplicate key: 1463048935
at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:186)
at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:141)
How do I make it ignore the duplicate entries, and just parse it to a map with any one from the duplicate entries?
Hackerman solution, tested and working using GSON v2.8.5, but use at your own risk! Whenever you update GSON to a new version, make sure to check whether this is still working!
Basically, you can use the fact that the generic ObjectTypeAdapter ignores duplicates as seen here:
Looks like MapTypeAdapterFactory checks for duplicate
V replaced = map.put(key, value);
if (replaced != null) {
throw new JsonSyntaxException("duplicate key: " + key);
}
however ObjectTypeAdapter does not
case BEGIN_OBJECT:
Map<String, Object> map = new LinkedTreeMap<String, Object>();
in.beginObject();
while (in.hasNext()) {
map.put(in.nextName(), read(in));
}
in.endObject();
return map;
What you can do now is trying to deserialize using fromJson as usual, but catch the "duplicate key" exception, deserialize as a generic Object, which will ignore duplicates, serialize it again, which will result in a JSON string without duplicate keys, and finally deserialize using the correct type it's actually meant to be.
Here is a Kotlin code example:
fun <T> String.deserialize(gson: Gson, typeToken: TypeToken<T>): T {
val type = typeToken.type
return try {
gson.fromJson<T>(this, type)
} catch (e: JsonSyntaxException) {
if (e.message?.contains("duplicate key") == true) {
gson.toJson(deserialize(gson, object : TypeToken<Any>() {})).deserialize(gson, typeToken)
} else {
throw e
}
}
}
Obviously, this adds (potentially heavy) overhead by requiring 2 deserializations and an additional serialization, but currently I don't see any other way to do this. If Google decides to add an option for a built-in way to ignore duplicates, as suggested here, better switch to that.
I couldn't use Kotlin (as answered before), so I adjusted it to Java
It could be achieved by registering the type adder:
#Test
void testDuplicatesIgnored() {
String json = "{\"key\": \"value\", \"key\": \"value2\"}";
Gson gson = new GsonBuilder()
.registerTypeAdapter(Map.class, new JsonDeserializer<Map<String, Object>>() {
#Override
public Map<String, Object> deserialize(JsonElement json1, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return new Gson().fromJson(json1, typeOfT);
}
})
.create();
Type mapType = new TypeToken<Map<String, Object>>() {}.getType();
Map<String, Object> map = gson.fromJson(json, mapType);
System.out.println("map = " + map); // map = {key=value2}
assertThat(map).hasSize(1);
assertThat(map.get("key")).isEqualTo("value2");
}
This way all the mappings to Map.class will go through your deserializer code
Yea, looks like a dirty hack, but it works
Another way is to register type adder for your custom type to make the deserializer being called only where you need it:
#Test
void testDuplicatesIgnored() {
String json = "{\"key\": \"value\", \"key\": \"value2\"}";
Gson gson = new GsonBuilder()
.registerTypeAdapter(MapIgnoringDuplicatesContainer.class, new JsonDeserializer<MapIgnoringDuplicatesContainer>() {
#Override
public MapIgnoringDuplicatesContainer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Type mapType = new TypeToken<Map<String, Object>>() {}.getType();
return new MapIgnoringDuplicatesContainer(new Gson().fromJson(json, mapType));
}
})
.create();
Map<String, Object> map = gson.fromJson(json, MapIgnoringDuplicatesContainer.class).getMap();
System.out.println("map = " + map); // map = {key=value2}
assertThat(map).hasSize(1);
assertThat(map.get("key")).isEqualTo("value2");
}
private class MapIgnoringDuplicatesContainer {
private Map<String, Object> map;
public MapIgnoringDuplicatesContainer(Map<String, Object> map) {
this.map = map;
}
public Map<String, Object> getMap() {
return map;
}
}
```