How do I marshall nested key,value pairs into JSON with Camel and Jackson library?

13,046

Your structure is a more than a map. It's two maps that are serialised differently. One way to represent this is:

public class Whatever{
  Map<String,String> keyvalues;
  Map<String,String> visibility;
}

What you'll end up with is this, which although represents the data is far from ideal:

{
 "keyvalues" : { "key1": "5", "key2": "10", "key3": "17"},
 "visibility" : { "key1": "a&b&!c", "key2": "a&b", "_default": "a" }
}

To get what you want, use @JsonAnyGetter. Something like this (it could be made much easier to use):

public class Whatever{
    Map<String,String> keyvalues = new TreeMap<String,String>();
    @JsonProperty
    Map<String,String> visibility = new TreeMap<String,String>();

    @JsonAnyGetter
    public Map<String, String> getKeyvalues() {
        return keyvalues;
    }
}

which produces:

           {"visibility":{"key1":"a&b&!c","key2":"a&b"},"key1":"5","key2":"10"}

I've been battling this today and your question inspired me to make it bloody work :D The annotations are here: https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations

See JUnit test here: https://gist.github.com/TomDemeranville/7009250

Share:
13,046
erj2code
Author by

erj2code

Updated on June 13, 2022

Comments

  • erj2code
    erj2code almost 2 years

    I have a Java project that currently returns a map to Camel without any nested key,value pairs, and the Jackson library marshalls it just fine into JSON using the Jackson library.

    For example if I put the following two key,values into a demoMap:

    Map<String,String> demoMap = new TreeMap<String,String>
    demoMap.put("key1","5");
    demoMap.put("key2","10");
    

    I get the following JSON:

    {"key1":"5","key2":"10"}
    

    However, now some of my key,value entries will have an optional visibility that I need to put as a nested key value in my map. Any key,values that don't have an optional visibility will use the default. So, for example if I specify visibilities for key1, and key2, but not key3 I want to get JSON out that looks like this:

    {"key1":"5",
     "key2":"10",
     "key3":"17",
     "visibility" : { "key1": "a&b&!c", "key2": "a&b", "_default": "a" }
    }
    

    How can I get Camel to marshall a Java object with nested key,value pairs? I'm a visual learner, so a simple example would be helpful.

    I tried changing my Map to have a value as an object i.e.,:

    Map<String,Object> demoMap = new TreeMap<String,Object>
    

    and then tried adding nested key,values for some keys with an ArrayList using http://examples.javacodegeeks.com/core-java/json/jackson/convert-java-map-to-from-json-using-jackson-example/ for reference, but realized that this just gives me a bunch of nested values under a key, not a bunch of nested key,value pairs.

    Even when I tried it for grins, I got an error from the Camel processor with a java.lang.ClassCastException stating java.util.ArrayList cannot be cast to java.lang.String

    And similarly when I tried to nest a Map inside my demoMap I got this ClassCastException:

    3244 [hello.world.request.timer] ERROR org.apache.camel.processor.DefaultErrorHandler  - Failed delivery for exchangeId: e6518e39-89b7-435e-96d9-ce26811ac67e. Exhausted after delivery attempt: 1 caught: java.lang.ClassCastException: java.util.HashMap cannot be cast to java.lang.String
    

    So I know how NOT to do it. :-/

    I re-read the Camel JSON documentation at http://camel.apache.org/json.html but as of this writing it doesn't specify an example with nested key,value pairs.

    UPDATE: Based on feedback from Tom I created two Maps i.e.,

    Map<String,String> keyvalues = new TreeMap<String,String>();
    Map<String,String> visibility = new TreeMap<String,String>();
    

    Here is my class which I call SensorGenerator that loads a properties file into a Map:

    package sample;
    
    import java.util.Map;
    import java.util.Properties;
    import java.util.TreeMap;
    
    import org.codehaus.jackson.annotate.JsonAnyGetter;
    import org.codehaus.jackson.annotate.JsonProperty;
    
    public class SensorGenerator {
    
        private Properties sourceProperties;
    
        // create a map of sensor keyvalues, and a map of sensor visibility
        Map<String,String> keyvalues = new TreeMap<String,String>();
        @JsonProperty
        Map<String,String> visibility = new TreeMap<String,String>();
    
        @JsonAnyGetter
        public Map<String, String> getKeyvalues() { 
    
            for (Object key : sourceProperties.keySet()) {
    
                // Separate out each of the field:datatype:visibility tuples as an entry in the
                // values array
                String[] values = sourceProperties.getProperty((String) key).split(
                        ",");
                // split the key between 'sensor' and the 'number' Ex: sensor1 -> sensor,1
                String[] keyArray = key.toString().split("(?<=([a-zA-Z]++))");
                String keyNumber = keyArray[1]; // grab the number to append for each sensor
    
                // define string buffer that appends sensor number for each sensor's
                // keys.  Ex: sensor1 would have s1make, s1makeDataType, etc.
                StringBuffer sensorNumberStringBuffer = new StringBuffer();
                sensorNumberStringBuffer.append("s");
                sensorNumberStringBuffer.append(keyNumber);
    
                // make, its data type, and visibility (with s# prefix)
                StringBuffer makeStringBuffer = new StringBuffer();
                makeStringBuffer.append(sensorNumberStringBuffer);
                makeStringBuffer.append("make");
                StringBuffer makeDataTypeStringBuffer = new StringBuffer();
                makeDataTypeStringBuffer.append(sensorNumberStringBuffer);
                makeDataTypeStringBuffer.append("makeDataType");
                StringBuffer makeVizStringBuffer = new StringBuffer();
                makeVizStringBuffer.append(sensorNumberStringBuffer);
                makeVizStringBuffer.append("makeViz");
    
                // model, its data type, and visibility (with s# prefix)
                StringBuffer modelStringBuffer = new StringBuffer();
                modelStringBuffer.append(sensorNumberStringBuffer);
                modelStringBuffer.append("model");
                StringBuffer modelDataTypeStringBuffer = new StringBuffer();
                modelDataTypeStringBuffer.append(sensorNumberStringBuffer);
                modelDataTypeStringBuffer.append("modelDataType");
                StringBuffer modelVizStringBuffer = new StringBuffer();
                modelVizStringBuffer.append(sensorNumberStringBuffer);
                modelVizStringBuffer.append("modelViz");
    
                // serialNumber, its data type, and visibility (with s# prefix)
                StringBuffer serialNumberStringBuffer = new StringBuffer();
                serialNumberStringBuffer.append(sensorNumberStringBuffer);
                serialNumberStringBuffer.append("serialNumber");
                StringBuffer serialNumberDataTypeStringBuffer = new StringBuffer();
                serialNumberDataTypeStringBuffer.append(sensorNumberStringBuffer);
                serialNumberDataTypeStringBuffer.append("serialNumberDataType");
                StringBuffer serialNumberVizStringBuffer = new StringBuffer();
                serialNumberVizStringBuffer.append(sensorNumberStringBuffer);
                serialNumberVizStringBuffer.append("serialNumberViz");
    
                // sensorType, its data type, and visibility (with s# prefix)
                StringBuffer sensorTypeStringBuffer = new StringBuffer();
                sensorTypeStringBuffer.append(sensorNumberStringBuffer);
                sensorTypeStringBuffer.append("sensorType");
                StringBuffer sensorTypeDataTypeStringBuffer = new StringBuffer();
                sensorTypeDataTypeStringBuffer.append(sensorNumberStringBuffer);
                sensorTypeDataTypeStringBuffer.append("sensorTypeDataType");
                StringBuffer sensorTypeVizStringBuffer = new StringBuffer();
                sensorTypeVizStringBuffer.append(sensorNumberStringBuffer);
                sensorTypeVizStringBuffer.append("sensorTypeViz");
    
                //  put all the field:datatype keyvalues for this sensor in the keyvalues map
                //  and visibilities in the visibility map
    
                // make, data type, and visibility
                keyvalues.put(makeStringBuffer.toString(), values[0].split(":")[0]);
                keyvalues.put(makeDataTypeStringBuffer.toString(), values[0].split(":")[1]);
                visibility.put(makeVizStringBuffer.toString(), values[0].split(":")[2]);
    
                // model, data type, and visibility
                keyvalues.put(modelStringBuffer.toString(), values[1].split(":")[0]);
                keyvalues.put(modelDataTypeStringBuffer.toString(), values[1].split(":")[1]);
                visibility.put(modelVizStringBuffer.toString(), values[1].split(":")[2]);
    
                // serialNumber, data type, and visibility
                keyvalues.put(serialNumberStringBuffer.toString(), values[2].split(":")[0]);
                keyvalues.put(serialNumberDataTypeStringBuffer.toString(), values[2].split(":")[1]);
                visibility.put(serialNumberVizStringBuffer.toString(), values[2].split(":")[2]);
    
                // sensorType, data type, and visibility
                keyvalues.put(sensorTypeStringBuffer.toString(), values[3].split(":")[0]);
                keyvalues.put(sensorTypeDataTypeStringBuffer.toString(), values[3].split(":")[1]);
                visibility.put(sensorTypeVizStringBuffer.toString(), values[3].split(":")[2]);
    
                // add in default visibility
                visibility.put("_default", "a");
    
            }
            return keyvalues;
        }
    
        public void setSourceProperties(Properties properties) {
            this.sourceProperties = properties;
        }
    
    }
    

    Right now I just hardcoded the default visibility to "a", but will change that later to also be pulled from a properties file.

  • erj2code
    erj2code over 10 years
    Tom, thank you for the information. I added in the code you suggested, but when I run the program I only see the keyvalues in the JSON. For some reason I don't see the nested visibility fields. I'll attach my SensorGenerator.java class, perhaps I'm missing something :-)
  • tom
    tom over 10 years
    here it is as a JUnit test. This test works for me. What version Jackson are you using? gist.github.com/TomDemeranville/7009250
  • erj2code
    erj2code over 10 years
    I'm using camel-jackson 2.12.1. I have the following dependency in my pom.xml: <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-jackson</artifactId> <version>${camel}</version> </dependency> where I define ${camel} above as 2.12.1
  • erj2code
    erj2code over 10 years
    I just noticed that I'm using different libraries that you are for JsonAnyGetter and JsonProperty. You are using: import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonProperty; and I'm using import org.codehaus.jackson.annotate.JsonAnyGetter; import org.codehaus.jackson.annotate.JsonProperty; Did you specify the com.fasterxml library in your pom.xml?
  • tom
    tom over 10 years
    You are using an old version. The new version uses the fasterxml namespace. I'm using Jackson 2.2.3 I think. You may have both versions on your classpath.
  • erj2code
    erj2code over 10 years
    Are you specifying that newer version in your pom.xml or your applicationContext.xml? I looked in both and I don't seem to specify a version for the Jackson library. I guess that's why its using an older version.
  • erj2code
    erj2code over 10 years
    Here's all I've currently specified in my applicationContext.xml for marshalling to JSON: <!-- Add Jackson library to render Java Map into JSON --> <camel:dataFormats> <camel:json id="jack" library="Jackson"/> </camel:dataFormats>
  • erj2code
    erj2code over 10 years
    Can you post your pom.xml and Camel applicationContext.xml files with your JUnit test? I want to see if I can figure out what to add to call the newer Jackson library. Thanks.
  • erj2code
    erj2code over 10 years
    Btw, I looked under my .classpath under my Eclipse project and found out that yes, I am specifying multiple versions of the Jackson library, i.e.: <classpathentry kind="var" path="M2_REPO/org/codehaus/jackson/jackson-asl/0.9.4/jackson‌​-asl-0.9.4.jar"/>
  • erj2code
    erj2code over 10 years
    And then further down in my .classpath I have: <classpathentry kind="lib" path="/home/accumulo/.m2/repository/org/codehaus/jackson/jac‌​kson-core-asl/1.8.8/‌​jackson-core-asl-1.8‌​.8.jar"> with a bunch of attributes set
  • tom
    tom over 10 years
    Mine is imported as a dependency of restlet 2.2-M3.
  • tom
    tom over 10 years
    I'm also using <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-csv</artifactId> <version>2.2.3</version> </dependency> which seems to be importing it as well. Part of my jackson is 2.1, other parts 2.2.3. Doh.
  • erj2code
    erj2code over 10 years
    I couldn't seem to get maven to pull down the fasterxml.jackson.library, so I downloaded the jackson-annotations-2.2.3.jar and added it to my classpath. It now points my SensorGenerator class to the same library as yours, but I still don't get the nested visibility
  • erj2code
    erj2code over 10 years
    Thanks for the info. I discovered some Jackson library conflicts in my pom.xml that I'm cleaning up.