0

Not for the faint of heart.

Trying to do a Scalyr integration, and wrestling with some pretty difficult JSON. I have used JSON2Apex which generated several inner classes, and the Apex and JSON are below. I'm trying to generate a map of metric_name to value, for each serverHost. The first two are listed under matches, and the latter under sessions, the two of which are connected by an ID (i.e. log_session_4181034113742147733).

The parsing is accomplished with:

ScalyrLog Log = (ScalyrLog) JSON.deserialize(JSONresponse, ScalyrLog.class);
        List<ScalyrLog.Matches> matches1 = Log.matches;                     
            Integer i = matches1[1].attributes.value;
            system.debug(i);

Or:

Map<String, Object> results = (Map<String, Object>)JSON.deserializeUntyped(response.getBody());
List<Object> matches2 = (List<Object>) results.get('matches');

But after that I'm having trouble getting started because of the way the inner classes are structured. If you can highlight a way to arrive at Map<serverHost,Map<metric_name,value>>, you are a sage.

"continuationToken" : "Pb6yJtUA/toV4P9In_GpAAAkNz9RZ2qP",
  "status" : "success",
  "matches" : [ {
    "timestamp" : "1576540556964768000",        
    "thread" : "log_2",
    "attributes" : {
      "dataset" : "json",
      "value" : 2119,
      "message_" : "METRIC",
      "version" : "2_12_3_RC1",
      "thread" : 140458843170560,
      "location" : "./gui/service.py:dump_metrics:4612",
      "plugin_unique_name" : "gui",
      "level" : "INFO",
      "metric_name" : "system.devices.seen",
      "@timestamp" : "2019-12-16T23:55:55.709Z",
      "original_file" : "/home/ubuntu/cortex/logs/gui/gui.43qgrreb.log"
    },
    "session" : "log_session_4181034113742147733",
    "severity" : 3
  }, {
    "timestamp" : "1576540556964824064",        
    "thread" : "log_2",
    "attributes" : {
      "dataset" : "json",
      "value" : 198,
      "message_" : "METRIC",
      "version" : "2_12_3_RC1",
      "thread" : 140458843170560,
      "location" : "./gui/service.py:dump_metrics:4615",
      "plugin_unique_name" : "gui",
      "level" : "INFO",
      "metric_name" : "system.users.seen",
      "@timestamp" : "2019-12-16T23:55:55.725Z",
      "original_file" : "/home/ubuntu/cortex/logs/gui/gui.brabr.log"
    },
    "session" : "log_session_4181034113742147733",
    "severity" : 3
  } ],
  "sessions" : {
    "log_session_4181034113742147733" : {
      "serverHost" : "node-vzhp_lax",
      "dataset" : "json",
      "serverIP" : "91.215.166.4",
      "logfile" : "/gui.axbearbr",
      "tier" : "customer",
      "parser" : "json"
    }
  },
  "cpuUsage" : 293,
  "executionTime" : 0
}

Apex:

public class ScalyrLog {

    public String continuationToken;
    public String status;
    public List<Matches> matches;
    public Sessions sessions;
    public Integer cpuUsage;
    public Integer executionTime;

    public class Sessions {
        public Log_session_4181034113742147733 log_session_4181034113742147733;
    }

    public class Log_session_4181034113742147733 {
        public String serverHost;
        public String dataset;
        public String serverIP;
        public String logfile;
        public String tier;
        public String parser;
    }

    public class Matches {
        public String timestamp;
        public String message;
        public String thread;
        public Attributes attributes;
        public String session;
        public Integer severity;
    }

    public class Attributes {
        public String dataset;
        public Integer value;
        public String message;
        public String version;
        public Long thread;
        public String location;
        public String plugin_unique_name;
        public String level;
        public String metric_name;
        public String original_file;
    }
}
3
  • N.B. You can name your inner classes however you want, for example Matches should really be Match because matchers property is a list of Match objects, class Log_session_4181034113742147733 could be named LogSession. After that, it is simply a matter of traversing the deserialized structure and pivoting it to what you need (which could be a new structure with new inner classes
    – cropredy
    Dec 17, 2019 at 1:00
  • @cropredy Hi, thanks, could you explain “traversing the deserialized structure and pivoting it to what you need”? I think that's what I'm really asking about.
    – number41
    Dec 17, 2019 at 15:08
  • ah, the nub of the problem is that log_session_4181034113742147733 as expressed in the JSON is a variable type; sfdcfox is right, you can't deserialize this as you outlined and will need to deserializeUnTyped
    – cropredy
    Dec 17, 2019 at 19:34

1 Answer 1

0

Change the following:

public Sessions sessions;

To:

public Map<String, Session> sessions;

And change:

public class Log_session_4181034113742147733 {

To:

public class Session {

At this point, when you deserialize to your wrapper, the sessions property will contain entries for each session. You can remove the previous "Sessions" class, as it is no longer used.

This is something that JSON2Apex can't automatically detect, but is easy enough to fix.


However, note that due to the fact that other attributes have invalid class names, you'll be forced to do manual parsing or JSON.deserializeUnTyped.

5
  • Thanks, but how does that get you to the goal? If you can’t use the JSON2Apex, how could you use Untyped? What I'm really trying to figure out is how to connect the data points that exist in such nested classes.
    – number41
    Dec 17, 2019 at 15:09
  • I can do "Map<String, Object> results = (Map<String, Object>)JSON.deserializeUntyped(response.getBody());" and then "List<Object> matches2 = (List<Object>) results.get('matches');", but how can I access the Attributes class after that?
    – number41
    Dec 17, 2019 at 17:56
  • 1
    @number41 you can't use classes directly when using untyped deserialization (because it's untyped, i.e. has no custom classes). For each element in matches2, cast to a Map<String, Object>. For example: Map<String, Object> match0 = (Map<String, Object>)matches[0]; Map<String, Object> attributes = (Map<String, Object>)match0.get('attributes');
    – sfdcfox
    Dec 17, 2019 at 18:18
  • 1
    Alternatively, if the JSON results are not too long you can replace the reserved names in both the Class and the string to have _X or similar then can deserialize into the class. i.e. String.replace('"value"','"value_X"'). All depends on how big the resultant JSON is expected to be. I find that a lot easier than the untyped route
    – Eric
    Dec 17, 2019 at 23:25
  • Thanks, just to clarify, that would mean running a String.replace on the JSON response string itself, before deserializing.
    – number41
    Dec 18, 2019 at 14:45

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .