JLP
JLP

Reputation: 311

Looking for Json-path/(any API) to update any value in given json string in Java

Inshort : I am trying to find some api that could just change the value by taking first parameter as jsonString , second parameter as JSONPath and third will be new value of that parameter. But, all I found is this.. https://code.google.com/p/json-path/

This api allows me to find any value in JSON String. But, I am not finding easy way to update the value of any key. For example, Here is a book.json.

{
"store":{
    "book":[
        {
            "category":"reference",
            "author":"Nigel Rees",
            "title":"Sayings of the Century",
            "price":8.95
        },
        {
            "category":"fiction",
            "author":"Evelyn Waugh",
            "title":"Sword of Honour",
            "price":12.99,
            "isbn":"0-553-21311-3"
        }
    ],
    "bicycle":{
        "color":"red",
        "price":19.95
    }
   }
 }

I can access color of bicycle by doing this.

String bicycleColor = JsonPath.read(json, "$.store.bicycle.color");

But I am looking for a method in JsonPath or other api some thing like this

    JsonPath.changeNodeValue(json, "$.store.bicycle.color", "green");
    String bicycleColor = JsonPath.read(json, "$.store.bicycle.color");
    System.out.println(bicycleColor);  // This should print "green" now. 

I am excluding these options,

Reason: I have about 500 different requests for different types of service which return different json structure. So, I do not want to manually create new JSON string always. Because, IDs are dynamic in json structure.

Any idea or direction is much appreciated.

Updating this question with following answer.

  1. Copy MutableJson.java.
  2. copy this little snippet and modify as per you need.

    private static void updateJsonValue() {
    
    JSONParser parser = new JSONParser();
    JSONObject jsonObject = new JSONObject();
    
    FileReader reader = null;
    try {
        File jsonFile = new File("path to book.json");
        reader = new FileReader(jsonFile);
        jsonObject = (JSONObject) parser.parse(reader);
    
    } catch (Exception ex) {
        System.out.println(ex.getLocalizedMessage());
    }
    
    Map<String, Object> userData = null;
    try {
        userData = new ObjectMapper().readValue(jsonObject.toJSONString(), Map.class);
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    
    MutableJson json = new MutableJson(userData);
    
    System.out.println("Before:\t" + json.map());
    
    json.update("$.store.book[0].author", "jigish");
    json.update("$.store.book[1].category", "action");
    
    System.out.println("After:\t" + json.map().toString());
    
    }
    

Use these libraries.

Upvotes: 13

Views: 19765

Answers (4)

Homyk
Homyk

Reputation: 364

The thing is that the functionality you want is already an undocumented feature of JsonPath. Example using your json structure:

String json = "{ \"store\":{ \"book\":[ { \"category\":\"reference\", \"author\":\"Nigel Rees\", \"title\":\"Sayings of the Century\", \"price\":8.95 }, { \"category\":\"fiction\", \"author\":\"Evelyn Waugh\", \"title\":\"Sword of Honour\", \"price\":12.99, \"isbn\":\"0-553-21311-3\" } ], \"bicycle\":{ \"color\":\"red\", \"price\":19.95 } } }";
DocumentContext doc = JsonPath.parse(json).
    set("$.store.bicycle.color", "green").
    set("$.store.book[0].price", 9.5);
String newJson = new Gson().toJson(doc.read("$"));

Upvotes: 15

kdabir
kdabir

Reputation: 9868

Just answering for folks landing on this page in future for reference.

You could consider using a Java implementation of jsonpatch. RFC can be found here

JSON Patch is a format for describing changes to a JSON document. It can be used to avoid sending a whole document when only a part has changed. When used in combination with the HTTP PATCH method it allows partial updates for HTTP APIs in a standards compliant way.

You can specify the operation that needs to be performed (replace, add....), json path at which it has to be performed, and the value which should be used.

Again, taking example from the RFC :

 [
     { "op": "test", "path": "/a/b/c", "value": "foo" },
     { "op": "remove", "path": "/a/b/c" },
     { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
     { "op": "replace", "path": "/a/b/c", "value": 42 },
     { "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
     { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
   ]

For Java implementation, I have not used it myself, but you can give a try to https://github.com/fge/json-patch

Upvotes: 2

Vlad
Vlad

Reputation: 1763

Assuming that parsed JSON can be represented in memory as a Map, you can build an API similar to JsonPath that looks like:

void update(Map<String, Object> json, String path, Object newValue);

I've quickly done a gist of a dirty implementation for simple specific paths (no support for conditions and wildcards) that can traverse json tree, E.g. $.store.name, $.store.books[0].isbn. Here it is: MutableJson.java. It definitely needs improvement, but can give a good start.

Usage example:

import java.util.*;

public class MutableJson {

    public static void main(String[] args) {
        MutableJson json = new MutableJson(
                new HashMap<String, Object>() {{
                    put("store", new HashMap<String, Object>() {{
                        put("name", "Some Store");
                        put("books", Arrays.asList(
                                new HashMap<String, Object>() {{
                                    put("isbn", "111");
                                }},
                                new HashMap<String, Object>() {{
                                    put("isbn", "222");
                                }}
                        ));
                    }});
                }}
        );

        System.out.println("Before:\t" + json.map());

        json.update("$.store.name", "Book Store");
        json.update("$.store.books[0].isbn", "444");
        json.update("$.store.books[1].isbn", "555");

        System.out.println("After:\t" + json.map());
    }

    private final Map<String, Object> json;

    public MutableJson(Map<String, Object> json) {
        this.json = json;
    }

    public Map<String, Object> map() {
        return json;
    }

    public void update(String path, Object newValue) {
        updateJson(this.json, Path.parse(path), newValue);
    }

    private void updateJson(Map<String, Object> data, Iterator<Token> path, Object newValue) {
        Token token = path.next();
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            if (!token.accept(entry.getKey(), entry.getValue())) {
                continue;
            }

            if (path.hasNext()) {
                Object value = token.value(entry.getValue());
                if (value instanceof Map) {
                    updateJson((Map<String, Object>) value, path, newValue);
                }
            } else {
                token.update(entry, newValue);
            }
        }
    }
}

class Path {
    public static Iterator<Token> parse(String path) {
        if (path.isEmpty()) {
            return Collections.<Token>emptyList().iterator();
        }
        if (path.startsWith("$.")) {
            path = path.substring(2);
        }

        List<Token> tokens = new ArrayList<>();
        for (String part : path.split("\\.")) {
            if (part.matches("\\w+\\[\\d+\\]")) {
                String fieldName = part.substring(0, part.indexOf('['));
                int index = Integer.parseInt(part.substring(part.indexOf('[')+1, part.indexOf(']')));
                tokens.add(new ArrayToken(fieldName, index));
            } else {
                tokens.add(new FieldToken(part));
            }
        };

        return tokens.iterator();
    }
}

abstract class Token {

    protected final String fieldName;

    Token(String fieldName) {
        this.fieldName = fieldName;
    }

    public abstract Object value(Object value);

    public abstract boolean accept(String key, Object value);

    public abstract void update(Map.Entry<String, Object> entry, Object newValue);
}

class FieldToken extends Token {

    FieldToken(String fieldName) {
        super(fieldName);
    }

    @Override
    public Object value(Object value) {
        return value;
    }

    @Override
    public boolean accept(String key, Object value) {
        return fieldName.equals(key);
    }

    @Override
    public void update(Map.Entry<String, Object> entry, Object newValue) {
        entry.setValue(newValue);
    }
}

class ArrayToken extends Token {

    private final int index;

    ArrayToken(String fieldName, int index) {
        super(fieldName);
        this.index = index;
    }

    @Override
    public Object value(Object value) {
        return ((List) value).get(index);
    }

    @Override
    public boolean accept(String key, Object value) {
        return fieldName.equals(key) && value instanceof List && ((List) value).size() > index;
    }

    @Override
    public void update(Map.Entry<String, Object> entry, Object newValue) {
        List list = (List) entry.getValue();
        list.set(index, newValue);
    }
}

A JSON string can be easily parsed into a Map using Jackson:

Map<String,Object> userData = new ObjectMapper().readValue("{ \"store\": ... }", Map.class);

Upvotes: 6

durron597
durron597

Reputation: 32343

So in order to change a value within a JSon string, there are two steps:

  1. Parse the JSon
  2. Modify the appropriate field

You are trying to optimize step 2, but understand that you are not going to be able to avoid step 1. Looking at the Json-path source code (which, really, is just a wrapper around Jackson), note that it does do a full parse of the Json string before being able to spit out the read value. It does this parse every time you call read(), e.g. it is not cached.

I think this task is specific enough that you're going to have to write it yourself. Here is what I would do:

  1. Create an object that represents the data in the parsed Json string.
    • Make sure this object has, as part of it's fields, the Json String pieces that you do not expect to change often.
  2. Create a custom Deserializer in the Json framework of your choice that will populate the fields correctly.
  3. Create a custom Serializer that uses the cached String pieces, plus the data that you expect to change

I think the exact scope of your problem is unusual enough that it is unlikely a library already exists for this. When a program receives a Json String, most of the time what it wants is the fully deserialized object - it is unusual that it needs to FORWARD this object on to somewhere else.

Upvotes: 0

Related Questions