jsongsonjsonreaderandroid

Transform and flatten JSON using GSON


I’m working on splitting a SignalK JSON object into canonical JSON items representing each value.

The original JSON looks like this:

{
"mmsi": "urn:mrn:signalk:uuid:5377770-4ee4-4a4b-3230-888037332031",
"name": "Mona",
"navigation": {
    "position": {
        "timestamp": "1991-09-03T03:5:36.000Z",
        "latitude": 51.763691,
        "longitude": 9.501367,
        "altitude": 0.000000,
        "source": "N0183-01"
    },
    "courseOverGroundTrue": {
        "value": 23.000000
    },
    "speedOverGround": {
        "value": 2.010289
    }
},
"environment": {
    "depth": {
        "belowTransducer": {
            "value": 12.700000
        }
    },
    "wind": {
        "angleApparent": {
            "value": 0.174533
        },
        "speedApparent": {
            "value": 0.000000
        }
    }}}

The needed transformed JSON looks like this, with JSON elements representing each value, and with item naming representing the whole path of the value.

{
"items": [{
        "columns": {
            "assetId": "urn:mrn:signalk:uuid:5377770-4ee4-4a4b-3230-888037332031",
            "description": "EnvironmentWindSpeedApparent",
            "isStep": false,
            "name": "EnvironmentWindSpeedApparent",
            "timestamps": 1523962903470,
            "type": "numerical",
            "values": 0.0
        },
        "key": "20180417-130143470EnvironmentWindSpeedApparent5377770-4ee4-4a4b-3230-888037332031"
    }, {
        "columns": {
            "assetId": "urn:mrn:signalk:uuid:5377770-4ee4-4a4b-3230-888037332031",
            "description": "EnvironmentWindAngleApparent",
            "isStep": false,
            "name": "EnvironmentWindAngleApparent",
            "timestamps": 1523962903470,
            "type": "numerical",
            "values": 0.174533
        },
        "key": "20180417-130143470EnvironmentWindAngleApparent5377770-4ee4-4a4b-3230-888037332031"
    }, {
        "columns": {
            "assetId": "urn:mrn:signalk:uuid:5377770-4ee4-4a4b-3230-888037332031",
            "description": "EnvironmentDepthBelowTransducer",
            "isStep": false,
            "name": "EnvironmentDepthBelowTransducer",
            "timestamps": 1523962903470,
            "type": "numerical",
            "values": 12.7
        },
        "key": "20180417-130143470EnvironmentDepthBelowTransducer5377770-4ee4-4a4b-3230-888037332031"
    }, {
        "columns": {
            "assetId": "urn:mrn:signalk:uuid:5377770-4ee4-4a4b-3230-888037332031",
            "description": "NavigationPositionLongitude",
            "isStep": false,
            "name": "NavigationPositionLongitude",
            "timestamps": 1523962903470,
            "type": "numerical",
            "values": 9.501367
        },
        "key": "20180417-130143470NavigationPositionLongitude5377770-4ee4-4a4b-3230-888037332031"
    }, {
        "columns": {
            "assetId": "urn:mrn:signalk:uuid:5377770-4ee4-4a4b-3230-888037332031",
            "description": "NavigationPositionLatitude",
            "isStep": false,
            "name": "NavigationPositionLatitude",
            "timestamps": 1523962903470,
            "type": "numerical",
            "values": 51.763691
        },
        "key": "20180417-130143470NavigationPositionLatitude5377770-4ee4-4a4b-3230-888037332031"
    }, {
        "columns": {
            "assetId": "urn:mrn:signalk:uuid:5377770-4ee4-4a4b-3230-888037332031",
            "description": "NavigationCourseOverGroundTrue",
            "isStep": false,
            "name": "NavigationCourseOverGroundTrue",
            "timestamps": 1523962903470,
            "type": "numerical",
            "values": 23.0
        },
        "key": "20180417-130143470NavigationCourseOverGroundTrue5377770-4ee4-4a4b-3230-888037332031"
    }, {
        "columns": {
            "assetId": "urn:mrn:signalk:uuid:5377770-4ee4-4a4b-3230-888037332031",
            "description": "NavigationSpeedOverGround",
            "isStep": false,
            "name": "NavigationSpeedOverGround",
            "timestamps": 1523962903470,
            "type": "numerical",
            "values": 2.010289
        },
        "key": "20180417-130143470NavigationSpeedOverGround5377770-4ee4-4a4b-3230-888037332031"
    }
    ]}

How to do this transformation in a flexible way that adopts to changing sub-nodes being available in the original JSON ? I’m transforming it now in a simplistic way, but would like to know if it can be done using JsonReader , gson or other ways of iterating through the original JSON object.


Solution

  • I ended up defining the object structure representing the transformed json, and writing a custom serializer to do the transformation. It could be more efficient ways of doing it, but this approach seems to be working OK.

    public class SignalKDeserializer implements JsonDeserializer<TargetObject> {
    //written based on examples from http://www.javacreed.com/gson-deserialiser-example/
    
    final TargetObject targetObject = new TargetObject ();
    
    @Override
    public TargetObject deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
            throws JsonParseException {
        final JsonObject jsonObject = json.getAsJsonObject();
        traverse (jsonObject,0,"", mmsi);
        return targetObject;
    
    }
    
    private void traverse (JsonObject jsonObject, Integer level, String parentName, String mmsi) {
        Set<String> keys = jsonObject.keySet();
        Iterator<?> keysIterator = keys.iterator ();
        while( keysIterator.hasNext ()) {
            String key = (String) keysIterator.next ();
            String signalName = parentName+upperCaseFirst (key); //setting signalName to complete path of value
            if (jsonObject.get (key) instanceof JsonObject) {
                traverse ((jsonObject.get (key)).getAsJsonObject (),level+1,upperCaseFirst (signalName),mmsi);
            } else if (jsonObject.get (key) instanceof JsonElement) {
                if (level>0) {
                    try {
                        final Double value = jsonObject.get(key).getAsDouble ();
                        calendar = Calendar.getInstance ();
                        Long timeStamp = calendar.getTimeInMillis ();
                        Item targetItem = new Item ();
                        targetItem.columns.setIsStep (false);
                        targetItem.columns.setAssetId (mmsi);
                        targetItem.columns.setTimestamps (calendar.getTimeInMillis ());
                        targetItem.columns.setType ("numerical");
                        targetItem.columns.setDescription (signalName);
                        targetItem.columns.setValues (value);
                        targetItem.columns.setName (signalName);
                        targetItem.setKey (signaldateformat.format (timeStamp) + mmsi);
                        targetObject.items.add (targetItem);
                    }
                    catch (NumberFormatException n) {
                        // Expected, the value is non numerical and  will not be transformed
                    }
                }
            }
        }
    }}
    

    I'm using the serializer from my main class like this:

    final GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(TargetObject.class, new SignalKDeserializer ());
    final Gson gson = gsonBuilder.create();
    TargetObject targetObject = gson.fromJson (jsonSignalK,TargetObject.class);
    String jsonOutString = gson.toJson (targetObject);
    

    jsonOutString contains the transformed json I needed.