I want to modify a java object by passing a json String to my application. The String will not contain all information about the complete, modified object but merely a single member that's meant to be set.
class SomeClass {
Object var1 = "Hello";
Object var2 = "AAA";
// A lot of fields goes here ...
}
public AppTest() throws Exception {
SomeClass myObject = new SomeClass();
myObject.var2 = "BBB";
String modification = "{\"var1\":\"Goodbye\"}";
Gson gson = new Gson();
SomeClass modifed = gson.fromJson(modification, SomeClass.class);
// TODO: Merge a modifed object into myObject somehow
}
Furthermore, some of the fields might be objects with any number of fields. Again, I might want to just modify a single primitive inside the child object. A more complex example:
class SomeOtherClass {
String var4 = "444";
String var5 = "555";
}
class SomeClass {
Object var1 = "111";
Object var2 = "222";
SomeOtherClass var3 = new SomeOtherClass();
}
public AppTest() throws Exception {
SomeClass myObject = new SomeClass();
myObject.var2 = "AAA";
myObject.var3.var5 = "BBB";
String modification = "{\"var3\":{\"var5\":\"XXX\"}}";
Gson gson = new Gson();
SomeClass modifed = gson.fromJson(modification, SomeClass.class);
// TODO: Merge the modifed object into myObject somehow
}
So, my question is how can I partially modify an object with JSON?
Hi I tried out one pseudo sample as below:
static Object merge(Object target, Object modified) {
for (Field f : target.getClass().getDeclaredFields()) {
if (!f.isAccessible()) {
f.setAccessible(true);
}
if (f.getType().isPrimitive()) {
try {
if (f.getType().isAssignableFrom(java.lang.Boolean.TYPE)
&& f.getBoolean(modified) != f.getBoolean(target)) {
f.setBoolean(target, f.getBoolean(modified));
} else if (f.getType().isAssignableFrom(java.lang.Character.TYPE)
&& f.getChar(modified) != f.getChar(target)) {
f.setChar(target, f.getChar(modified));
} else if (f.getType().isAssignableFrom(java.lang.Integer.TYPE)
&& f.getInt(modified) != f.getInt(target)) {
f.setInt(target, f.getInt(modified));
}
//....
// do it for all other primitive types
//also consider Enum types(not primitive so check 'f.getType().isEnum()')
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
} else if (f.getType().getPackage().getName().matches("java.lang.*")
|| f.getType().getPackage().getName().matches("java.util.*")) {
/* Here I am trying to directly assign changes for the basic packages, if you want more you can add more packages*/
try {
if (f.get(modified) != null && f.get(target) != f.get(modified)) {
f.set(target, f.get(modified));
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
} else {
/* If local classes encountered as member variables then do the same merge!*/
try {
merge(f.get(target), f.get(modified));
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return target;
}
you can call this method as myObject = (SomeClass) merge(myObject, modifed);
Note: It is not a fully functional method to do your job, you please read the comments inline and make it as a perfect for your case. I only ensure basic functionality
Managed to create a recursive method for replacing properties in a JsonObject.
private static void mergeObjects(JsonObject object, JsonObject modification) {
// Iterate through the modified properties
for (Entry<String, JsonElement> entry : modification.entrySet()) {
JsonElement je = entry.getValue();
// If the modified property is an object, iterate through the properties of the modified property
if (je instanceof JsonObject) {
JsonObject nextOrigObject = object.get(entry.getKey()).getAsJsonObject();
JsonObject nextModObject = je.getAsJsonObject();
mergeObjects(nextOrigObject, nextModObject);
}
// If the modified property is not an object, set the original object to match the modified property
else
object.add(entry.getKey(), je);
}
}
With this method, I can merge two objects as follows:
class SomeClass {
Object var1 = "Hello";
Object var2 = "AAA";
}
public TestApplication() {
SomeClass myObject = new SomeClass();
myObject.var2 = "BBB";
String modificationString = "{\"var1\":\"Goodbye\"}";
Gson gson = new Gson();
JsonObject original = gson.fromJson(gson.toJson(myObject), JsonObject.class);
JsonObject modification = gson.fromJson(modificationString, JsonObject.class);
mergeObjects(original, modification);
myObject = gson.fromJson(original, SomeClass.class);
System.out.println(myObject.var1); // Prints "Goodbye"
}
public static void main(String[] args) {
new DummyFile();
}
Comments?
There might be prettier ways to convert the SomeClass object to a JsonObject before the merging, feel free to add your suggestions.
EDIT: added else in merging method
EDIT2: added comments
Any Problem in doing this
modifed.var2=myObject.var2;
String modificationStr = gson.toJson(modifed);
System.out.println(modificationStr);
Related
Given a class Person:
String name;
String surname;
String id;
String address;
I have an object obj1 with the following values:
name="Name"
surname=null
id="ABC123"
address="Here"
Through an api call I get the following json:
{
"name":"John",
"surname":"Doe",
"id":"A1B2C3"
}
which gets mapped into an object obj2like this:
name="John"
surname="Doe"
id="A1B2C3"
address=null
I want to copy all non-null (or empty string) values of obj2 into obj1 so the final result is this:
name="John"
surname="Doe"
id="A1B2C3"
address="Here"
I have two problems.
The first one is that I don't want to have to manually type the get/set call for each attribute of the object.
The second problem, is that I want the method to work for any type of object with no or minimal changes.
At the very least, I need the first problem solved. The second one is optional, but would be great to learn a solution too.
You can use reflection to get all the instance fields and use Field#set to copy over non-null values.
try {
for (Field field : Person.class.getDeclaredFields()) {
if (!Modifier.isStatic(field.getModifiers())) {
field.setAccessible(true);
Object val = field.get(obj2);
if (val != null) {
field.set(obj1, val);
}
}
}
System.out.println(obj1);
} catch (IllegalAccessException e) {
// Handle exception
}
Demo
public static void main(String[] args) {
MyClass obj1 = new MyClass(1,"1");
MyClass obj2 = new MyClass(2,"2");
System.out.println(obj2);
copy(obj1, obj2);
System.out.println(obj2);
}
public static MyClass copy(MyClass obj1, MyClass obj2){
Arrays.stream(obj1.getClass().getDeclaredFields()).forEach(f -> {
try {
f.set(obj2, f.get(obj1));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
return obj2;
}
static class MyClass {
int myInt;
String myString;
String emptyString = null;
public MyClass(int myInt, String myString) {
this.myInt = myInt;
this.myString = myString;
}
#Override
public String toString() {
return "MyClass{" +
"myInt=" + myInt +
", myString='" + myString + '\'' +
", emptyString='" + emptyString + '\'' +
'}';
}
}
in case you have private fields you can use
f.setAccessible(true);
but I don't suggest that.
Reading your question, seems like, you have two classes, in two distinct projects, which, exchange information in json.
So, It is easy if you can use Jackson(default in spring)
You could annotate the target class with the annotation of jackson #JsonInclude(JsonInclude.Include.NON_NULL)
I am using Jackson for de/serialization in my app.
I have a situation where I need to convert a JSON string to one of my 3 classes. In case the string can't be converted to either one of 3 classes, it will considered to be an unrecognized case.
However, if the schema of json string and the provided class in mapper.readValue(jsonString,MyClass1.class) does not match, it throws an UnrecognizedPropertyException.
Currently I am using something like below, but it seems to be pretty messy.
try {
obj = mapper.readValue(jsonString, MyClass1.class);
} catch (UnrecognizedPropertyException e1) {
try {
obj = mapper.readValue(jsonString, MyClass2.class);
} catch (UnrecognizedPropertyException e2) {
try {
obj = mapper.readValue(jsonString, MyClass3.class);
} catch (Exception e) {
//handle unrecognized string
}
} catch (Exception e) {
//handle unrecognized string
}
} catch (Exception e) {
//handle unrecognized string
}
Is this how it needs to be done or is there any other alternative? Is there any way to configure the mapper to return null in case of unrecognized properties, as that would result in creating a simple series if blocks instead of nested try-catch blocks?
You can try this method to do deserialization thing. this will return null on UnrecognizedPropertyException:
private <T> T deserialize(ObjectMapper mapper, Class<T> type, String jsonString) {
T t = null;
try {
t = mapper.readValue(jsonString, type);
} catch (UnrecognizedPropertyException e) {
//handle unrecognized string
}catch (IOException e) {
//handle under errors
}
return t;
}
If jsonString is generated by you, you can consider to add type info and then use it to convert deserialized object. You could refer to this post for how to do it.
If jsonString is generated by other services beyond your control, then there's no type info you can get so you can only try it one by one, #Sachin Gupta's answer would be a nice choice.
I'd like to provide an additional option: define an all-in-one entity including all fields of MyClass1, MyClass2 and MyClass3, and make MyClass1, MyClass2 and MyClass3 be separated wrapper and only expose related fields for each. Code as follows:
Class AllInOne:
public class AllInOne {
protected String a;
protected String b;
protected String c;
public A asA() {
return new A(this);
}
public B asB() {
return new B(this);
}
public C asC() {
return new C(this);
}
}
Class A:
public class A {
private AllInOne allInOne;
public A(AllInOne allInOne) {
this.allInOne = allInOne;
}
public String getA() {
return allInOne.a;
}
}
Class B:
public class B {
private AllInOne allInOne;
public B(AllInOne allInOne) {
this.allInOne = allInOne;
}
public String getB() {
return allInOne.b;
}
}
Class C:
public class C {
private AllInOne allInOne;
public C(AllInOne allInOne) {
this.allInOne = allInOne;
}
public String getC() {
return allInOne.c;
}
}
Test code:
public class Main {
public static void main(String[] args) throws IOException {
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
String jsonA = "{\"a\":\"a value\"}";
String jsonB = "{\"b\":\"b value\"}";
String jsonC = "{\"c\":\"c value\"}";
needTypeA(om.readValue(jsonA, AllInOne.class).asA());
needTypeB(om.readValue(jsonB, AllInOne.class).asB());
needTypeC(om.readValue(jsonC, AllInOne.class).asC());
}
private static void needTypeA(A a) {
System.out.println(a.getA());
}
private static void needTypeB(B b) {
System.out.println(b.getB());
}
private static void needTypeC(C c) {
System.out.println(c.getC());
}
}
With implementation like this, we erased the specific type info at deserialization step, and bring it back at the moment we really need/use it. And as you can see there's not too much extra code, because what we actually did is just moving all fields declaration together, and added couple methods.
Notes:
I declare fields in AllInOne to be protected, putting all POJO class in the same package will make A, B and C be able to access them directly, but not for other classes outside.
Setting om.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); to make jackson deserialize by field, so that we can remove duplicate setter and getter from AllInOne class
If you do need to know the type info, you could add methods like isA inside AllInOne based on the fields info
If json contains some define property, than you can try to use #JsonTypeInfo and #JsonSubTypes. Classes MyClass1, ... must implement this interface. Also I don`t remember exactly how to map unknown implementations to null.
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY, // level of define property
property = <property_name>,
visible = true,
defaultImpl = NoClass.class)
#JsonSubTypes({#JsonSubTypes.Type(value = <interface-impl>.class, name = <property_value>)})
private <interface> value;
// getters and setters
I have the below member.
protected List<FieldHandler> fieldHandlers;
This list contains a list of concrete fieldHandler objects. Below is how values are added to this list.
fieldHandlers.add(new StringFieldHandler(fieldName));
Below is the FieldHandler object and a concrete implementation example.
public interface FieldHandler<T,R> {
R handle(R target, T fieldValue) throws JSONMapperException;
}
public class IntegerFieldHandler extends ValueFieldHandler<Integer,JSONObject> {
public IntegerFieldHandler(String fieldName) {
super(fieldName);
}
#Override
public JSONObject handle(JSONObject target, Integer fieldValue) throws JSONMapperException {
try {
target.put(fieldName, fieldValue);
} catch (JSONException ex) {
throw new JSONMapperException(ex);
//log the exception
}
return target;
}
}
Now I'm trying to get values from this list. Which could eventually be StringFieldHandlers or IntegerFieldHandlers or BooleanFieldHandlers.
FieldHandler fieldHandlerForCurrentHBaseColumnValue = fieldHandlers.get(i);
I need to check which handler this is and call the relevant handle method passing in the value.
I tried doing it this way but it seems ugly.
JSONObject targetObject = null;
FieldHandler fieldHandlerForCurrentHBaseColumnValue = fieldHandlers.get(i);
if (fieldHandlerForCurrentHBaseColumnValue instanceof IntegerFieldHandler) {
IntegerFieldHandler integerFieldHandler = (IntegerFieldHandler) fieldHandlerForCurrentHBaseColumnValue;
targetObject = integerFieldHandler.handle(targetObject,(Integer) tuple.get(i));
} else if (fieldHandlerForCurrentHBaseColumnValue instanceof BooleanFieldHandler) {
} else if (fieldHandlerForCurrentHBaseColumnValue instanceof StringFieldHandler) {
}
How can I do this? Any help would be much appreciated.
I have a DTO with some GSon annotation.
My probleme is that the value of these annotations have to change if my application run in developpement or in staging or in production...
For the moment, I have to package my application with the different value and I want this to be automatic... It is in a Spring Boot application and I want to use the spring.profiles.active to tell my application to take the right serializedName
Here is the kind of code I use
// Tests
// #SerializedName("customfield_10123")
// Prod
#SerializedName("customfield_10114")
private ActionDto action;
I hope there is a better way to do it?
Here is a very crude example on how you can achieve what you want:
First create a propery file for each possible profile (name can be anything, but the profile must be on the name):
application-dev.properties
application-prod.properties
...
Populate the properties with the values you want for each key accordingly to each profile:
test=abc.test
...
Annotate your POJOs:
public class Foo {
#SerializedName("${test}")
private String name;
...
}
Create a custom serializer for your class, which will interpret the custom names, something like this:
public class FooSerializer implements JsonSerializer<Foo> {
private static final Pattern PATTERN = Pattern.compile("\\$\\{(.*)\\}");
private static Properties props;
static {
try {
Resource resource = new ClassPathResource(String.format("/application-%s.properties", System.getProperty("spring.profiles.active")));
props = PropertiesLoaderUtils.loadProperties(resource);
} catch (IOException e) {
e.printStackTrace();
}
}
#Override
public JsonElement serialize(Foo foo, Type type, JsonSerializationContext jsonSerializationContext) {
Field[] fields = foo.getClass().getDeclaredFields();
JsonObject object = new JsonObject();
for (Field field : fields) {
field.setAccessible(true);
String name = field.getName();
if (field.isAnnotationPresent(SerializedName.class)) {
String value = field.getAnnotation(SerializedName.class).value();
Matcher matcher = PATTERN.matcher(value);
if (matcher.find()) {
name = props.get(matcher.group(1)).toString();
} else {
name = value;
}
}
try {
if (field.get(foo) != null) {
object.addProperty(name, field.get(foo).toString());
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return object;
}
}
Now you just need to register your custom serializer and you are good to go:
Gson gson = new GsonBuilder().registerTypeAdapter(Foo.class, new FooSerializer()).setPrettyPrinting().create();
Of course there may be better ways to recover the properties file according to the active profile, but the given snippet should be enough to get you going. Also, you need to consider the fact that there may be multiple profiles active at any given time, so if that is your scenario, you need to take it into consideration before recovering the properties.
You don't even need the regex part if you will always want to use the value from the properties. I used a regex to allow both cases.
If something wasn't clear, please let me know and I will try to improve it.
EDIT:
For the deserialization I can't come up with anything very good, so here is an example which I think is far from OK, but gets the job done:
Functional interface:
public interface Converter {
Object convert(String s);
}
Deserializer:
public class FooDeserializer implements JsonDeserializer<Foo> {
private static final Pattern PATTERN = Pattern.compile("\\$\\{(.*)\\}");
private static Properties props;
private static Map<Class, Converter> converterForClass = new HashMap<>();
static {
try {
Resource resource = new ClassPathResource(String.format("/application-%s.properties", System.getProperty("spring.profiles.active")));
props = PropertiesLoaderUtils.loadProperties(resource);
converterForClass.put(Integer.TYPE, s -> Integer.parseInt(s.replace("\"", "")));
converterForClass.put(Double.TYPE, s -> Double.parseDouble(s.replace("\"", "")));
converterForClass.put(String.class, s -> s);
converterForClass.put(Long.TYPE, s -> Long.parseLong(s.replace("\"", "")));
converterForClass.put(Boolean.TYPE, s -> Boolean.parseBoolean(s.replace("\"", "")));
} catch (IOException e) {
e.printStackTrace();
}
}
#Override
public Foo deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
Foo foo = new Foo();
JsonObject jobject = (JsonObject) jsonElement;
for (Entry entry : jobject.entrySet()) {
Field field = searchField(entry.getKey().toString());
if (field != null) {
field.setAccessible(true);
try {
Object r = converterForClass.get(field.getType()).convert(entry.getValue().toString());
field.set(foo, r);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return foo;
}
private Field searchField(String name) {
Field[] fields = Foo.class.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
if (field.isAnnotationPresent(SerializedName.class)) {
String value = field.getAnnotation(SerializedName.class).value();
Matcher matcher = PATTERN.matcher(value);
if (value.equals(name)) {
return field;
} else if (matcher.find()) {
if (props.get(matcher.group(1)).equals(name)) {
return field;
}
}
} else {
if (field.getName().equals(name)) {
return field;
}
}
}
return null;
}
Register the deserializer:
gsonBuilder.registerTypeAdapter(Foo.class, new FooDeserializer());
The problem with the above approach is it will NOT work with nested objects. You will have to some further verifications and implementation. It is using Java 8 features as well.
My json string looks like the following:
{
"text": ["foo",1,"bar","2",3],
"text1": "value1",
"ComplexObject": {
.....
}
}
I have a pojo defined like this:
class MyPojo {
List<String> text;
String text1;
ComplexObject complexObject;
}
I use google gson and am able to get my java object populated properly. The problem here is that the field text is an array of mixed types (string and int). So all the entries there are converted into String and i am not able to figure out which entries in the array is a string vs int. I cant use parseInt since the entries in the original array may have "2" as well as 3.
Is there a way for me to get the right instance type of the fields in my array after converting into java object.
SOLUTION
So i implemented the solution using gson the round about way using the JsonDeserializer. And then i tried using jackson. Guess what jackson supports serializing/deserializing the mixed array type by preserving the data types.
ObjectMapper mapper = new ObjectMapper();
MyPojo gmEntry = mapper.readValue(json, new TypeReference<MyPojo >(){});
And i can basically fetch the List<Object> and do an instanceof to check for the datatype.
Shame on you gson!!
By having a custom class and adding a type adapter u can manipulate the string (json.toString() returns with the '"' quotes, so you can see if its a string or not.
Output: (the classes seem correct)
class test.Main$StringPojo pojo{object=foo}
class test.Main$IntPojo pojo{object=1}
class test.Main$StringPojo pojo{object=bar}
class test.Main$StringPojo pojo{object=2}
class test.Main$IntPojo pojo{object=3}
public static void main(final String[] args){
String str = "{\n" +
" \"text\": [\"foo\",1,\"bar\",\"2\",3],\n" +
" \"text1\": \"value1\" }";
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(pojo.class, new JsonDeserializer<pojo>() {
#Override
public pojo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
try {
return new IntPojo(Integer.parseInt(json.toString()));
} catch (Exception e) {
return new StringPojo(json.getAsString());
}
}
});
MyPojo myPojo = builder.create().fromJson(str, MyPojo.class);
for (pojo pojo : myPojo.text) {
System.out.println(pojo.getClass() + " " + pojo.object);
}
}
public static abstract class pojo{
protected Object object;
public pojo() {
}
#Override
public String toString() {
return "pojo{" +
"object=" + object +
'}';
}
}
public static class StringPojo extends pojo{
public StringPojo(String str) {
object = str;
}
}
public static class IntPojo extends pojo{
public IntPojo(int intt) {
this.object = intt;
}
}
public static class MyPojo {
List<pojo> text;
String text1;
}
As you wrote - you defined: List<String> text; but that list also contains integers.
Java is strongly typed, please consider to either declare the List as List<Object> (less preferable) or creating a JSON list that contains only a single type of variable (more preferable).
You can create an abstract class ItemType (for use as array item type) and inherits from it two wrapper classes: one for int type and another for string type.
abstract class ItemType {
protected Object value;
}
class IntType extends ItemType {
IntType(Integer value){
this.value = value;
}
}
class StringType extends ItemType {
IntType(String value){
this.value = value;
}
}
Try this List<ItemType> text;
The above situation can be achived by using TypeAdapter of Gson API.
Please follow : https://sites.google.com/site/gson/gson-user-guide#TOC-Serializing-and-Deserializing-Collection-with-Objects-of-Arbitrary-Types
Not sure if this is what you need, but this is the code I use for parsing JSON.
static public void newsParser(String urlString, String targetObject) throws FileNotFoundException, IOException
{
URL url = new URL(urlString);
JSONParser parser=new JSONParser();
BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));
Object obj;
try
{
obj = parser.parse(br);
//JSONObject jsonObject = (JSONObject) obj;
JSONArray jsonArray = (JSONArray) obj;
Iterator<?> i = jsonArray.iterator();
while (i.hasNext())
{
slide = (JSONObject) i.next();
newsInfo = (String)slide.get(targetObject);
System.out.println(newsInfo);
newsTitles.add(newsInfo);
}
}
catch (ParseException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}