1

I am trying to get a single nested value from a ResponseEntity but I am trying to do so without having to create a pojo for every possible item as this is a third party api response.

Example response.getBody() as it appears in Postman:

{
    "message": "2 records found",
    "records": [
        {
            "Account": {
                "Id": "1",
                "Name": "Foo Inc"
            },
            "CaseNumber": "200",
            "Contact": {
                "FirstName": "Foo",
                "LastName": "Bar"
            },
            "Status": "In Progress",
            "StatusMessage": "We are working on this."
        },
        {
            "Account": {
                 "Id": "1",
                 "Name": "Foo Inc"
            },
            "CaseNumber": "100",
            "Contact": {
                "FirstName": "Foo",
                "LastName": "Bar"
            },
            "Status": "Closed"
        }

    ]
}

Basically, if I were in JS, I am looking for:

for(let record of res.body.records){
   if(record && record.CaseNumber === "200"){
      console.log(record.Status)
}
res.body.records[0].Status

Currently, they are are doing this to check if the response is empty:

ResponseEntity<Object> response = restTemplate.exchange(sfdcURL, HttpMethod.POST, entity, Object.class);

LinkedHashMap<Object, Object> resMap = (LinkedHashMap<Object, Object>) response.getBody();
        
List<Object> recordsList = (List<Object>) resMap.get("records");

if (recordsList.size() <= 0) { return error }

But I need to get the value of of "Status" and I need to do so without creating a pojo.

I appreciate any guidance on how I can do this in Java

UPDATE

So the response.getBody() is returned and when it is displayed in Postman, it looks like the pretty JSON shown above. However, when I do:

System.out.println(response.getBody().toString())

it looks like:

{message=2 Records Found, records=[{Account={Id=1, Name=Foo Inc}, CaseNumber=200, Contact={FirstName=Foo, LastName=Bar}, //etc

To make it worse, one of the fields appears in the console as follows (including linebreaks):

[...], Status=In Progress, LastEmail=From: noreply@blah.com
Sent: 2022-08-08 10:14:54
To: foo@bar.com
Subject: apropos case #200


Hello Foo,
We are working on your case and stuff

Thank you,
us, StatusMessage=We are working on this., OtherFields=blah, [...]

text.replaceAll("=", ":") would help some, but won't add quotations marks nor would it help separate that email block.

How can I so that the responses here like ObjectMapper and JSONObject can work?

AylaWinters
  • 1,121
  • 2
  • 7
  • 24
  • Is it a Web-application? Are you using any framework? (if yes, it should provide some library for parsing JSON which you can leverage) – Alexander Ivanchenko Dec 30 '22 at 01:53
  • It is a webapp, We are using SpringBoot. I also updated my question to reflect the actual response of my ResponseEntity.toString() – AylaWinters Dec 30 '22 at 15:45
  • *"I need to do so without creating a pojo"* - that looks like an artificial requirement. While calling `restTemplate.exchange()` instead of `Object.class` you can provide the proper type. Is there any reason why need to process the data "without creating a pojo"? – Alexander Ivanchenko Dec 30 '22 at 17:37
  • *"when it is displayed in Postman, it looks like the pretty JSON shown above"* - Postman shows you the JSON **as is**, it doesn't alter the JSON contents, there's no problem with *"=", ":"* (`=` is coming from a `Map`, `RestTemplate` gives you a processed data, not a row JSON). If there are redundant new lines, they can be handled. See the answer for more details. – Alexander Ivanchenko Dec 30 '22 at 20:20

5 Answers5

1

You can either convert the string to valid json (not that trivial) and deserialise into a Map<String, Object>, or just pluck the value out of the raw string using regex:

String statusOfCaseNumber200 = response.getBody().toString()
  .replaceAll(".*CaseNumber=200\\b.*?\\bStatus=([^,}]*).*", "$1");

This matches the whole string, captures the desired status value then replaces with the status, effectively "extracting" it.

The regex:

  • .*CaseNumber=200\b everything up to and including CaseNumber=200 (not matching longer numbers like 2001)
  • .*? as few chars as possible
  • \\bStatus= "Status=" without any preceding word chars
  • ([^,}]*) non comma/curly brace characters
  • .* the rest

It's not bulletproof, but it will probably work for your use case so it doesn't need to be bulletproof.


Some test code:

String body = "{message=2 Records Found, records=[{Account={Id=1, Name=Foo Inc}, CaseNumber=200, Contact={FirstName=Foo, LastName=Bar}, Status=In Progress, StatusMessage=We are working on this.}, {Account={Id=1, Name=Foo Inc}, CaseNumber=100, Contact={FirstName=Foo, LastName=Bar}, Status=Closed}]";
String statusOfCaseNumber200 = body.replaceAll(".*CaseNumber=200\\b.*?\\bStatus=([^,}]*).*", "$1");
System.out.println(statusOfCaseNumber200); // "In Progress"
Bohemian
  • 412,405
  • 93
  • 575
  • 722
  • Thank you, I did think about "extracting" via regex, but I'm not sure my boss will go for that because of it not being bulletproof. Could you extrapolate on the converting the string to valid JSON? I tried just a simple replaceAll, but the email addition to the response throws all of that off. Especially since on of the values I need comes after the email value. – AylaWinters Jan 03 '23 at 14:45
  • 1
    @AylaWinters converting to json, by inserting quotation marks and punctuation etc, is not bulletproof either, and is *more* risky than json regex to extract the status. Unless the status value will have commas or curly brackets in its value (very unlikely), this approach *is* bulletproof. It defines status as "characters between `Status=` and the next comma/brace". I've simplified the regex too btw. The answer to your boss is write lots of unit tests using every combo of response you can think of. – Bohemian Jan 03 '23 at 19:56
  • Yes, manually inserting quotation mark etc would be awful and bullet-attracting even! haha Thank you again for your explanations and providing the regex breakdown! I posted a way that I might have to use, but it would also make changes my boss won't like, so I will let them decide between that and your regex solution! – AylaWinters Jan 04 '23 at 13:56
0

PLEASE DO NOT use Genson as Hiran showed in his example. The library hasn't been updated since 2019 and has many vulnerable dependencies!

Use Jackson or Gson.

Here how you can serialize a string into a Jackson JsonNode:

ObjectMapper mapper = new ObjectMapper();

String json = ...;
JsonNode node = mapper.readTree(json);

If you want to serialize a JSON object string into a Map:

ObjectMapper mapper = new ObjectMapper();

String json = ...;
Map<String, Object> map = mapper.readValue(json, HashMap.class);

You can read more about JsonNode here and a tutorial here.

Oliver
  • 1,465
  • 4
  • 17
  • Thank you for this. Unfortunately, it does not work because ResponseEntity is not yet in pretty JSON apparently. I updated my post for clarification, but ObjectMapper is giving me: Unexpected character 'm': was expecting double-quote to start field name – AylaWinters Dec 30 '22 at 15:44
0

You can use JSON-Java library and your code will look like this:

JSONObject jsonObject = new JSONObject(JSON_STRING);
String status = jsonObject.getJSONArray("records")
                .getJSONObject(0)
                .getString("Status");
System.out.println(status);

Or in a loop

JSONArray jsonArray = new JSONObject(jsonString).getJSONArray("records");
for(int i =0; i < jsonArray.length(); i++) {
    String status = jsonArray
                .getJSONObject(i)
                .getString("Status");
    System.out.println(status);
}
Harry Coder
  • 2,429
  • 2
  • 28
  • 32
  • I do like this approach and the loop, but the ResponseEntity.toString() is not yet in proper JSON so it keeps giving me expectation errors. I updated my post to explain in more detail – AylaWinters Dec 30 '22 at 15:41
0

So the response.getBody() is returned and when it is displayed in Postman, it looks like the pretty JSON shown above. However, when I do:

...

text.replaceAll("=", ":") would help some, but won't add quotations marks nor would it help separate that email block.

How can I so that the responses here like ObjectMapper and JSONObject can work?

Firstly, Jackson is the default message converter which Spring Web uses under the hood to serialize and deserialize JSON. You don't need to introduce any dependencies.

Secondly, the process serialization/deserialization is handled by the framework automatically, so that in many cases you don't need to deal with the ObjectMapper yourself.

To emphasize, I'll repeat: in most of the cases in Spring you don't need to handle raw JSON yourself. And in the body of ResponseEntiry<Object> produced by the method RestTemplate.exchange() you have a LinkedHashMap in the guise of Object, it's not a raw JSON (if you want to know why it is a LinkedHashMap, well because that's how Jackson stores information, and it's a subclass of Object like any other class in Java). And sure, when you're invoking toString() on any implementation of the Map you'll get = between a Key and a Value.

So, the problem you've mentioned in the updated question is artificial.

If you want to deal with a Map instead of an object with properly typed properties and here's how you can do that:

RestTemplate restTemplate = new RestTemplate();
        
ResponseEntity<LinkedHashMap<String, Object>> response = restTemplate.exchange(
    sfdcURL, HttpMethod.POST, entity, new ParameterizedTypeReference<>() {}
);
    
Map<String, Object> resMap = response.getBody();
    
List<Object> recordsList = (List<Object>) resMap.get("records");
    
if (recordsList.isEmpty()) { ... }

If there are redundant lines in the Values which you want to trim, then as a remedy you can introduce a custom Jackson-module declaring a Deserializer which would handle leading/trailing white-space and new lines, described in this answer. Deserialize in the module would be applied by default, other options would require creating classes representing domain objects which you for some reasons want to avoid.

Abra
  • 19,142
  • 7
  • 29
  • 41
Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
  • The code you provided is ultimately doing what they have already implemented to find out if the recordsList is empty or not. I am basically trying map the each record in recordsList if it is not empty so that I can find the value of "Status" and "StatusMessage" which would both be inside of "records". Also, I'm not sure that toString part is artificial. I see that toString will replace : with = , but when I try to use JSONObject or JSONArray, I get: "org.json.JSONException: Expected a ':' ". Thanks for your help! – AylaWinters Jan 03 '23 at 13:59
0

As Oliver suggested JsonNode seems to be the best approach. But, if I receive the ResponseEntity<Object>, I still cannot figure out a way to convert it to readable Json (and thus convert it to JsonNode), so I am still open to responses for that part.

I was able to get it to work by changing the ResponseEntity<Object> to ResponseEntity<JsonNode> so this is what I will be submitting for now:

ResponseEntity<JsonNode> response = restTemplate.exchange(sfdcURL,
      HttpMethod.POST, entity, JsonNode.class);

JsonNode records = response.getBody().get("records");

String status = null;
String statusMessage = null;

for (JsonNode rec : records) {
    if(rec.get("CaseNumber").asText().equals(caseNumber)) {
        status = rec.get("Status").asText();
        if(rec.has("StatusMessage")) {
            statusMessage = rec.get("StatusMessage").asText();
        }
    } else {
        statusMessage = "Invalid CaseNumber";
    }
}

Because the overall method returns a ResponseEntity<Object> I then converted my strings to a HashMap and returned that:

HashMap<String, String> resMap = new HashMap<String, String>();
resMap.put("Status", status);
resMap.put("StatusMessage", statusMessage);
        
return new ResponseEntity<>(resMap, HttpStatus.OK);

This is not a perfect solution, but it works for now. Would still be better for exception handling if I could receive a ResponseEntity<Object> and then convert it to a JsonNode though. Thanks everyone for the responses!

AylaWinters
  • 1,121
  • 2
  • 7
  • 24