How to get key from the json and preserve the order - json

I have a JSON with some key-value pairs. I need to pick up by name and process it's value differently. The oder of the process should be same order as the keys listed in the JSON (JSON is unknown, only knows it will have some key and corresponding value, so cant not know the order outside of the JSON).
Simplified sample usage:
String jsonData = "{key1:{some other data}, key2:{sub-key:sub-value}}";
val jsonObject = JSONObject(jsonData)
val names = jsonObject.keys()
if (names.hasNext()) {
val key = names.next()
val valueJson = jsonData.optString(key)
val gson = Gson()
val resultList = ArrayList<Data> () // result should keep the order as the keys in the json
if (key == "key1") {
val parsedResult = gson.fromJson <POJO_1> (valueJson, POJO_1::class.java)
resultList.add(processDataFromKey1(parsedResult))
} else if (key == "key2") {
val parsedResult = gson.fromJson <POJO_2> (valueJson, POJO_2::class.java)
resultList.add(processDataFromKey2(parsedResult))
}
}
The problem is the jsonObject.keys() the order of the keys is undefined.
So how to preserve the order of the keys from the JSON string (don't want to add dependency javax.json:javax.json-api)

org.json.JSONObject internally uses HashMap. They even added some comment to it:
HashMap is used on purpose to ensure that elements are unordered by
the specification. JSON tends to be a portable transfer format to
allows the container implementations to rearrange their items for a
faster element retrieval based on associative access. Therefore, an
implementation mustn't rely on the order of the item.
Use Gson. It uses com.google.gson.internal.LinkedTreeMap implementation for Map. From it's documentation:
A map of comparable keys to values. Unlike TreeMap, this class
uses insertion order for iteration order. Comparison order is only
used as an optimization for efficient insertion and removal.
This implementation was derived from Android 4.1's TreeMap class.
So, for below JSON payload:
{
"status": "status",
"date": "01/10/2019",
"abc": {
"Field1": "value1",
"key": "value2"
},
"rgj": {
"key": "value2",
"Field1": "value3"
},
"name": "Rick"
}
This example app:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.io.File;
import java.io.FileReader;
import java.lang.reflect.Type;
import java.util.Map;
public class GsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
Gson gson = new GsonBuilder().create();
Type type = new TypeToken<Map<String, Object>>() {
}.getType();
Map<String, Object> map = gson.fromJson(new FileReader(jsonFile), type);
map.forEach((k, v) -> {
if (v instanceof Map) {
System.out.println(k);
((Map) v).forEach((k1, v1) -> System.out.println(" " + k1 + " => " + v1));
} else {
System.out.println(k + " => " + v);
}
});
}
}
Prints:
status => status
date => 01/10/2019
abc
Field1 => value1
key => value2
rgj
key => value2
Field1 => value3
name => Rick
Notice that key and Field1 are in different order in abc and rgj objects.

you could use jsonparser rather than jsonreader so you control the event flow.
See https://docs.oracle.com/javaee/7/api/javax/json/stream/JsonParser.html
Note that your question is neither kotlin nor gson specific.

Another thing you can do is refer to the String jsonData in your example and rebuild the order.
Something like:
JSONObject json = new JSONObject(jsonData);
Iterator<String> keys = json.keys();
List<KeyOrder> ordering = new ArrayList();
while(keys.hasNext()) {
String key = keys.next();
int loc = jsonData.indexOf('"' + key + '"');
ordering.add(new KeyOrder(loc, key));
}
Collections.sort(ordering);
for(KeyOrder order: ordering) {
Object value = json.get(order.getKey());
// process the key and value
}
public class KeyOrder implements Comparable<KeyOrder> {
private int order;
private String key;
public KeyOrder(int order, String key) {
this.order = order;
this.key = key;
}
#Override
public int compareTo(KeyOrder o) {
return order - o.getOrder();
}
private int getOrder() {
return order;
}
public String getKey() {
return key;
}
}

Related

How to convert a complex object, having a HashMap parameter, into JSON and back? [duplicate]

I get the error:
Exception in thread "main" com.google.gson.JsonParseException:
Expecting object found: "com.shagie.app.SimpleMap$Data#24a37368"
when trying to deseralize a Map that uses non-trivial keys:
package com.shagie.app;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.HashMap;
public class SimpleMap {
public static void main(String[] args) {
Wrapper w = new Wrapper();
w.m.put(new Data("f", 1), new Data("foo", 3));
w.m.put(new Data("b", 2), new Data("bar", 4));
GsonBuilder gb = new GsonBuilder();
gb.setPrettyPrinting();
Gson g = gb.create();
String json = g.toJson(w);
System.out.println(json);
w = g.fromJson(json, Wrapper.class);
System.out.println(w.m.isEmpty());
}
static public class Wrapper {
HashMap<Data, Data> m = new HashMap<Data, Data>();
}
static public class Data {
String s;
Integer i;
public Data(String arg, Integer val) { s = arg; i = val; }
}
}
This serializes to the json:
{
"m": {
"com.shagie.app.SimpleMap$Data#24a37368": {
"s": "foo",
"i": 3
},
"com.shagie.app.SimpleMap$Data#66edc3a2": {
"s": "bar",
"i": 4
}
}
}
One can see the key attempting to be serialized, but certainly not in a way that can be deserialized.
How does one serialize this object so that it can be deserialized?
I found the following while trying to solve this puzzle: Issue 210: Cannot serialize or deserialize Maps with complex keys.
For any internet travelers from the future (like myself)... you can enable this functionality in GSON 2.* with the enableComplexMapKeySerialization() method on GsonBuilder.
Here's the javadoc for that method.
When enabled, the map will be serialized (and correctly deserialized) as an array of [key, value] arrays:
{"m":[[{"s":"f", "i",1}, {"s":"foo", "i":3}], [{"s":"b", "i",2}, {"s":"bar", "i":4}]]}
The problem is that toString() is getting called on the keys to the map, rather than them being serialized themselves.
To fix this a custom serializer and deserializer needs to be set up, and the deserializer needs to be aware of the format that the object uses to display itself as a string (the toString() method must return a string that can be used to reconstruct the entire object).
For the above example:
package com.shagie.app;
import com.google.gson.*;
import java.lang.reflect.Type;
import java.util.HashMap;
public class SimpleMapFixed {
public static void main(String[] args) {
Wrapper w = new Wrapper();
w.m.put(new Data("f", 1), new Data("foo", 3));
w.m.put(new Data("b", 2), new Data("bar", 4));
GsonBuilder gb = new GsonBuilder();
gb.setPrettyPrinting();
gb.registerTypeAdapter(Data.class, new DataSerializer());
Gson g = gb.create();
String json = g.toJson(w);
System.out.println(json);
w = g.fromJson(json, Wrapper.class);
System.out.println(w.m.isEmpty());
}
static public class Wrapper {
HashMap<Data, Data> m = new HashMap<Data, Data>();
}
static public class DataSerializer implements JsonSerializer<Data>,
JsonDeserializer<Data> {
#Override
public Data deserialize(JsonElement je, Type t, JsonDeserializationContext ctx)
throws JsonParseException {
Data rv;
JsonObject jo;
System.out.println("deserialize called with: " + je.toString());
if (je.isJsonObject()) {
jo = je.getAsJsonObject();
rv = new Data(jo.get("s").getAsString(), jo.get("i").getAsInt());
} else {
String js = je.getAsString();
String[] s = js.split(":", 2); // split into two (and only two)
rv = new Data(s[1], Integer.valueOf(s[0]));
}
System.out.println("deserialize returns: " + rv.s + " " + rv.i);
return rv;
}
#Override
public JsonElement serialize(Data data, Type type, JsonSerializationContext jsonSerializationContext) {
JsonObject jo = new JsonObject();
jo.addProperty("s", data.s);
jo.addProperty("i", data.i);
System.out.println("serialize called: " + jo.toString());
return jo;
}
}
static public class Data {
String s;
Integer i;
public Data(String arg, Integer val) { s = arg; i = val; }
#Override
public String toString() {
String rv = i.toString() + ':' + s;
System.out.println("toString called: " + rv);
return rv;
}
}
}
Running this code produces:
serialize called: {"s":"foo","i":3}
toString called: 1:f
serialize called: {"s":"bar","i":4}
toString called: 2:b
{
"m": {
"1:f": {
"s": "foo",
"i": 3
},
"2:b": {
"s": "bar",
"i": 4
}
}
}
deserialize called with: "1:f"
deserialize returns: f 1
deserialize called with: {"s":"foo","i":3}
deserialize returns: foo 3
deserialize called with: "2:b"
deserialize returns: b 2
deserialize called with: {"s":"bar","i":4}
deserialize returns: bar 4
Note the invocations of toString() as part of the serialization. In this code, the logic for the deserializion from the String form is in the DataSerializer, though it may make sense to move it into the Data class as another constructor instead - it doesn't affect the final outcome.
Further note that Data was a rather simple object itself with no deeper structures. Trying to serialize that as the key would require additional work.
Its Up to you how you are maintaining the HahMap Keys, You can deserialized it with simple and easiest way.
final Type typeOf = new TypeToken <Map<String, Map<String, Data>>>(){}.getType();
final Map<String, Map<String, Data>> newMap = gson.fromJson(json, typeOf);
final Map<String, Data> map = newMap.get("m");
final Iterator<Entry<String, Data>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String,Data> pair = (Map.Entry<String,Data>) it.next();
String key = pair.getKey();
System.out.println("key "+ key + " Values[ i= " + data.getI() + ", s= " +data.getS()+" ]");
}
Result:
key = snippet.Snippet$Data#61506150 Values [ i= 3, s= foo ]
key = snippet.Snippet$Data#63ff63ff Values [ i= 4, s= bar ]

Parsing empty object with kotlinx.serialization

I am struggling to understand how to parse an empty object {} with the experimental kotlinx.serialization library. The complication arises when in fact an API response can be one of;
{
"id": "ABC1",
"status": "A_STATUS"
}
or
{}
The data structure I have used as my serializer is;
data class Thing(val id: String = "", val status: String = "")
This is annotated with #kotlinx.serialization.Serializable and used within an API client library to marshall between the raw API response and the data model. The default values tell the serialisation library that the field is optional and replaces the #Optional approach of pre-Kotlin 1.3.30.
Finally, the kotlinx.serialization.json.Json parser I am using has the configuration applied by using the nonstrict template.
How do I define a serializer that can parse both an empty object and the expected data type with kotlinx.serialization? Do I need to write my own KSerialiser or is there config I am missing. Ideally, the empty object should be ignored/parsed as a null?
The error I get when parsing an empty object with my Thing data class is;
Field 'id' is required, but it was missing
So this was down to the kotlinCompilerClasspath having a different version kotlin (1.3.21, not 1.3.31).
Interestingly this was owing to advice I followed when configuring my gradle plugin project to not specify a version for the kotlin-dsl plugin.
Explicitly relying on the version I needed fixed the kotlinx.serialisation behavior (no changes to the mainline code)
Yes, ideally null instead of {} is way more convenient to parse but sometimes you just need to consume what backend sends you
There are 2 solutions that come to my mind.
Simpler, specific to your case using map:
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class ThingMapSerializerTest {
#Test
fun `should deserialize to non empty map`() {
val thingMap: Map<String, String> =
Json.decodeFromString("""{"id":"ABC1","status":"A_STATUS"}""")
assertTrue(thingMap.isNotEmpty())
assertEquals("ABC1", thingMap["id"])
assertEquals("A_STATUS", thingMap["status"])
}
#Test
fun `should deserialize to empty map`() {
val thingMap: Map<String, String> = Json.decodeFromString("{}")
assertTrue(thingMap.isEmpty())
}
}
More complex but more general that works for any combinations of value types. I recommend sealed class with explicit empty value instead of data class with empty defaults:
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.serialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Test
class ThingSerializerTest {
#Test
fun `should deserialize to thing`() {
val thing: OptionalThing =
Json.decodeFromString(
OptionalThing.ThingSerializer,
"""{"id":"ABC1","status":"A_STATUS"}"""
)
assertEquals(OptionalThing.Thing(id = "ABC1", status = "A_STATUS"), thing)
}
#Test
fun `should deserialize to empty`() {
val thing: OptionalThing =
Json.decodeFromString(OptionalThing.ThingSerializer, "{}")
assertEquals(OptionalThing.Empty, thing)
}
sealed class OptionalThing {
data class Thing(val id: String = "", val status: String = "") : OptionalThing()
object Empty : OptionalThing()
object ThingSerializer : KSerializer<OptionalThing> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("your.app.package.OptionalThing") {
element("id", serialDescriptor<String>(), isOptional = true)
element("status", serialDescriptor<String>(), isOptional = true)
}
override fun deserialize(decoder: Decoder): OptionalThing {
decoder.decodeStructure(descriptor) {
var id: String? = null
var status: String? = null
loop# while (true) {
when (val index = decodeElementIndex(descriptor)) {
CompositeDecoder.DECODE_DONE -> break#loop
0 -> id = decodeStringElement(descriptor, index = 0)
1 -> status = decodeStringElement(descriptor, index = 1)
else -> throw SerializationException("Unexpected index $index")
}
}
return if (id != null && status != null) Thing(id, status)
else Empty
}
}
override fun serialize(encoder: Encoder, value: OptionalThing) {
TODO("Not implemented, not needed")
}
}
}
}
When 'Thing' is a field within json object:
"thing":{"id":"ABC1","status":"A_STATUS"} // could be {}
you can annotate property like that:
#Serializable(with = OptionalThing.ThingSerializer::class)
val thing: OptionalThing
Tested for:
classpath "org.jetbrains.kotlin:kotlin-serialization:1.4.10"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"

Handle JSON which sends array of items but sometimes empty string in case of 0 elements

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

Map<String, HashSet<String>> to JSON, & Pretty Print

I'm trying to make my dataset correspond to this example:
var family = [{
"name" : "Jason",
"age" : "24",
"gender" : "male"
},
{
"name" : "Kyle",
"age" : "21",
"gender" : "male"
}];
I have a Map<String, HashSet<String>> of Names and unique alpha-numeric values correponding to specific entities to which those names could refer, let's call these entry items "IDs".
So for instance, Fyodor Mikhailovich Dostoyevsky would perhaps be related to the ID Q626, because that's a very specific reference, there aren't many widely known figures with that name. Whereas, Bush might be attached to G027, Q290, and Q118, referencing perhaps the man, the beer, and the shrub, in no particular order.
It looks like this (the real one is much bigger):
[Rao=[Q7293658, , Q7293657, Q12953055, Q3531237, Q4178159, Q1138810, Q579515, Q3365064, Q7293664, Q1133815], Hani Durzy=[], Louise=[, Q1660645, Q130413, Q3215140, Q152779, Q233203, Q7871343, Q232402, Q82547, Q286488, Q156723, Q3263649, Q456386, Q233192, Q14714149, Q12125864, Q57669, Q168667, Q141410, Q166028], Reyna=[Q7573462, Q2892895, Q363257, Q151944, Q3740321, Q2857439, Q1453358, Q7319529, Q733716, Q16151941, Q7159448, Q5484172, Q6074271, Q1753185, Q7319532, Q5171205, Q3183869, Q1818527, Q251862, Q3840414, Q5271282, Q5606181]]
Using Jackson I tried like this:
Map<String, HashSet<String>> map = q_valMap;
mapper.writeValue(new File("JSON_Output/user.json"), map);
But this seems wrong, as my output was all jumbled together, i.e.
{"Rao":["Q7293658","","Q7293657","Q12953055","Q3531237","Q4178159","Q1138810","Q579515","Q3365064","Q7293664","Q1133815"],"Hani Durzy":[""],"Louise":["","Q1660645","Q130413","Q3215140","Q152779","Q233203","Q7871343","Q232402","Q82547","Q286488","Q156723","Q3263649","Q456386","Q233192","Q14714149","Q12125864","Q57669","Q168667","Q141410","Q166028"],"Reyna":["Q7573462","Q2892895","Q363257","Q151944","Q3740321","Q2857439","Q1453358","Q7319529","Q733716","Q16151941","Q7159448","Q5484172","Q6074271","Q1753185","Q7319532","Q5171205","Q3183869","Q1818527","Q251862","Q3840414","Q5271282","Q5606181"]}
Do I just have to populate this JSON object iteratively?
Like the example up top, I think it should look something like this, though what follows is only a pseudocodish characterization, which is to say, not exactly this but something similar:
{
key: "Rao"
value: ["Q7293658","","Q7293657","Q12953055","Q3531237","Q4178159","Q1138810","Q579515","Q3365064","Q7293664","Q1133815"]
key: "Hani Durzy"
value: [""]
key: "Louise"
value: ["","Q1660645","Q130413","Q3215140","Q152779","Q233203","Q7871343","Q232402","Q82547","Q286488","Q156723","Q3263649","Q456386","Q233192","Q14714149","Q12125864","Q57669","Q168667","Q141410","Q166028"]
key: "Reyna"
value: ["Q7573462","Q2892895","Q363257","Q151944","Q3740321","Q2857439","Q1453358","Q7319529","Q733716","Q16151941","Q7159448","Q5484172","Q6074271","Q1753185","Q7319532","Q5171205","Q3183869","Q1818527","Q251862","Q3840414","Q5271282","Q5606181"]
}
is that not right?
UPDATE
public class JsonMapFileExample
{
public static void map(Map<String, HashSet<String>> q_valMap )
{
ObjectMapper mapper = new ObjectMapper();
ArrayNode array = mapper.createArrayNode();
for ( Entry entry: q_valMap.entrySet() )
{
ObjectNode node = mapper.createObjectNode()
.put("name", entry.getKey())
.put("ids", entry.getValue());
array.add(node);
}
mapper.writeValue("/home/matthias/Workbench/SUTD/nytimes_corpus/wdtk-parent/wdtk-examples/JSON_Output/user.json", array);
}
}
class MyEntity
{
private String name;
Set<String> value; // use names that you want in the result JSON
//constructors
public MyEntity()
{
}
public MyEntity(String name)
{
this.name = name;
}
//getters
public String getName()
{
return this.name;
}
public Set<String> getValue()
{
return this.value;
}
//setters
public void setName(String name)
{
this.name = name;
}
public void setValue(Set<String> value)
{
this.value = value;
}
}
You could manually set the key names, something like:
ArrayNode array = mapper.createArrayNode();
for (Entry entry: yourMap.entries()) {
ObjectNode node = mapper.createObjectNode()
.put("name", entry.key())
.putPOJO("ids", entry.value());
array.add(node);
}
mapper.writeValue(file, array);
Alternatively, you could create a class for your data
class MyEntity {
String name;
Set<String> ids; // use names that you want in the JSON result
// getters, setters if necessary
}
Transform your data map into a list of MyEntity, then use Jackson ObjectMapper to create JSON like mapper.writeValue(file, listOfMyEntities), the output would be like
[
{
"name": "some name here",
"ids": ["id1", "id2", ...]
}
// more elements here
]
how about this:
String name_list_file = "/home/matthias/Workbench/SUTD/nytimes_corpus/NYTimesCorpus/2005/01/02/test/people_test.txt";
String single_name;
try (
// read in the original file, list of names, w/e
InputStream stream_for_name_list_file = new FileInputStream( name_list_file );
InputStreamReader stream_reader = new InputStreamReader( stream_for_name_list_file , Charset.forName("UTF-8"));
BufferedReader line_reader = new BufferedReader( stream_reader );
)
{
while (( single_name = line_reader.readLine() ) != null)
{
//replace this by a URL encoder
//String associated_alias = single_name.replace(' ', '+');
String associated_alias = URLEncoder.encode( single_name , "UTF-8");
String platonic_key = single_name;
System.out.println("now processing: " + platonic_key);
Wikidata_Q_Reader.getQ( platonic_key, associated_alias );
}
}
//print the struc
Wikidata_Q_Reader.print_data();
}

Grails - how to let a domain class convert JSON into a domain property

I want to teach my domain class to automatically convert the results of JSON.parse(someJSON) into a member that is also a custom domain class.
Given these domain classes:
class Person {
Long id
String name
static hasMany = [aliases: PersonAlias]
}
class PersonAlias {
Person person
Long id
String name
}
And this JSON representing a Person with some PersonAliases:
{
"id":20044397,
"name":"John Smith",
"aliases":[{"id":13376,"name":"Johnny Smith"},{"id":13377,"name":"J. Smith"}]
}
I want to keep the controller simple like:
class PersonController {
def saveViaAjax = {
def props = JSON.parse(params.JSON)
Person p = Person.get(props.id)
p.properties = props
p.save(flush: true)
}
}
But sadly I get this error:
Failed to convert property value of type
'org.codehaus.groovy.grails.web.json.JSONArray' to required type
'java.util.Set' for property 'aliases'; nested exception is
java.lang.IllegalStateException: Cannot convert value of type
[org.codehaus.groovy.grails.web.json.JSONObject] to required type
[heavymeta.PersonAlias] for property 'aliases[0]': no matching editors
or conversion strategy found
So, I want to teach my domain class to how to convert the JSON data into PersonAlias instances automatically. I'd like to avoid formatting the data in the controller before passing it to the Domain object. How do I accomplish these goals?
You can use the bindUsing annotation and provide your custom binding code to convert the json to the property being bound.
class Person {
Long id
String name
#BindUsing({obj, source ->
List retVal = []
def aliases = source['aliases']
if(aliases) {
aliases.each {
retVal << new PersonAlias(name:it.name)
}
}
return retVal
})
List<PersonAlias> aliases
static hasMany = [aliases: PersonAlias]
}
I think this plugin: https://github.com/pedjak/grails-marshallers might do what you're looking for? I have not tried it myself though.
I also encountered this problem - I did my best to document the fix on my website - See http://dalelotts.com/software-architect/grails
In general the solution is to convert the JSON to a parameter map that can be used for data binding. More info on the site, including an annotation driven DomainClassMarshaller for JSON
protected Object readFromJson(Class type, InputStream entityStream, String charset) {
def mapper = new ObjectMapper();
def parsedJSON = mapper.readValue(entityStream, typeRef);
Map<String, Object> map = new HashMap<>();
parsedJSON.entrySet().each {Map.Entry<String, Object> entry ->
if (List.isAssignableFrom(entry.getValue().getClass())) {
List values = (List) entry.getValue();
int limit = values.size()
for (int i = 0; i < limit; i++) {
final theValue = values.get(i)
map.put(entry.key + '[' + i + ']', theValue)
appendMapValues(map, theValue, entry.key + '[' + i + ']' )
}
} else {
map.put(entry.key, entry.value);
}
}
def result = type.metaClass.invokeConstructor(map)
// Workaround for http://jira.codehaus.org/browse/GRAILS-1984
if (!result.id) {
result.id = idFromMap(map)
}
result
}
private void appendMapValues(Map<String, Object> theMap, Object theValue, String prefix) {
if (Map.isAssignableFrom(theValue.getClass())) {
Map<String, Object> valueMap = (Map<String, Object>) theValue;
for (Map.Entry<String, Object> valueEntry : valueMap.entrySet()) {
theMap.put(prefix + '.' + valueEntry.key, valueEntry.value)
appendMapValues(theMap, valueEntry.value, prefix + '.' + valueEntry.key)
}
}
}