28

Looking through the documentation and source code I don't see a clear way to do this. Curious if I'm missing something.

Say I receive an InputStream from a server response. I create a JsonParser from this InputStream. It is expected that the server response is text containing valid JSON, such as:

{"iamValidJson":"yay"}

However, if the response ends up being invalid JSON or not JSON at all such as:

Some text that is not JSON

the JsonParser will eventually throw an exception. In this case, I would like to be able to extract the underlying invalid text "Some text that is not JSON" out of the JsonParser so it can be used for another purpose.

I cannot pull it out of the InputStream because it doesn't support resetting and the creation of the JsonParser consumes it.

Is there a way to do this?

Puja
  • 192
  • 9
cottonBallPaws
  • 21,220
  • 37
  • 123
  • 171
  • Note that you can ask a `JsonParser` _not_ to close the underlying input stream. – fge May 30 '13 at 00:44

5 Answers5

27

If you have the JsonParser then you can use jsonParser.readValueAsTree().toString().

However, this likely requires that the JSON being parsed is indeed valid JSON.

Shadow Man
  • 3,234
  • 1
  • 24
  • 35
  • 1
    We use this method in a `JsonDeserializer` to use the `org.geotools.geojson.geom.GeometryJSON` parser to parse `GeoJSON` objects as it just parses the JSON `String` directly. – Shadow Man May 30 '13 at 00:15
  • "However, this likely requires that the JSON being parsed is indeed valid JSON" <-- yes it does. Jackson can be quite forgiving, but not that forgiving ;) – fge May 30 '13 at 00:43
  • If you are setting your `Content-Type` to `application/json` then you really should be sending JSON formatted content. Maybe you can change it to `{ "textDescription": "text" }` substituting "textDescription" for whatever makes sense in your particular case. – Shadow Man May 30 '13 at 00:57
  • Note however that Jackson can parse any JSON value, it is not limited to JSON text (ie, only arrays or objects). In fact, most parsers can, even though the RFC theoretically forbids that. – fge May 30 '13 at 01:00
  • 1
    Fyi, a quoted string or a plain number or even the constant values `true`, `false` and `null` are all valid JSON. See [JSON Values](https://tools.ietf.org/html/rfc7159#section-3) – Charlie Reitzel Jan 27 '20 at 16:12
6

I had a situation where I was using a custom deserializer, but I wanted the default deserializer to do most of the work, and then using the SAME json do some additional custom work. However, after the default deserializer does its work, the JsonParser object current location was beyond the json text I needed. So I had the same problem as you: how to get access to the underlying json string.

You can use JsonParser.getCurrentLocation.getSourceRef() to get access to the underlying json source. Use JsonParser.getCurrentLocation().getCharOffset() to find the current location in the json source.

Here's the solution I used:

public class WalkStepDeserializer extends StdDeserializer<WalkStep> implements
    ResolvableDeserializer {

    // constructor, logger, and ResolvableDeserializer methods not shown

    @Override
    public MyObj deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
            JsonProcessingException {
        MyObj myObj = null;

        JsonLocation startLocation = jp.getCurrentLocation();
        long charOffsetStart = startLocation.getCharOffset();

        try {
            myObj = (MyObj) defaultDeserializer.deserialize(jp, ctxt);
        } catch (UnrecognizedPropertyException e) {
            logger.info(e.getMessage());
        }

        JsonLocation endLocation = jp.getCurrentLocation();
        long charOffsetEnd = endLocation.getCharOffset();
        String jsonSubString = endLocation.getSourceRef().toString().substring((int)charOffsetStart - 1, (int)charOffsetEnd);
        logger.info(strWalkStep);

        // Special logic - use JsonLocation.getSourceRef() to get and use the entire Json
        // string for further processing

        return myObj;
    }
}

And info about using a default deserializer in a custom deserializer is at How do I call the default deserializer from a custom deserializer in Jackson

Community
  • 1
  • 1
user2636014
  • 61
  • 1
  • 1
5

5 year late, but this was my solution:

I converted the jsonParser to string

String requestString = jsonParser.readValueAsTree().toString();

Then I converted that string into a JsonParser

JsonFactory factory = new JsonFactory();
JsonParser parser  = factory.createParser(requestString);

Then I iterated through my parser

ObjectMapper objectMapper = new ObjectMapper();
while(!parser.isClosed()){
    JsonToken jsonToken = parser.nextToken();
    if(JsonToken.FIELD_NAME.equals(jsonToken)){
        String currentName = parser.getCurrentName();
        parser.nextToken();
        switch (currentName) {
            case "someObject":
                Object someObject = objectMapper.readValue(parser, Object.class)
                //validate someObject
                break;
        }
 }

I needed to save the original json string for logging purposes, which is why I did this in the first place. Was a headache to find out, but finally did it and I hope i'm helping someone out :)

adbar
  • 438
  • 1
  • 8
  • 18
  • This was very helpful. Doing this within my `StdDeserializer` extension registered with the `ObjectMapper`, I had to do this: `final String result = jp.readValueAsTree().toString(); jp = OBJECT_MAPPER.getFactory().createParser(result); final JsonNode root = OBJECT_MAPPER.getFactory().getCodec().readTree(jp);` Otherwise, the codec is null in a new factory. – rizard Sep 04 '18 at 20:56
4

Building my own Deserialiser in which I wanted to deserialise a specific field as text i.s.o. a proper DTO, this is the solution I came up with.

I wrote my own JsonToStringDeserializer like this:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringEscapeUtils;

import java.io.IOException;

/**
 * Deserialiser to deserialise any Json content to a String.
 */
@NoArgsConstructor
public class JsonToStringDeserializer extends JsonDeserializer<String> {


    /**
     * Deserialise a Json attribute that is a fully fledged Json object, into a {@link String}.
     * @param jsonParser Parsed used for reading JSON content
     * @param context Context that can be used to access information about this deserialization activity.
     * @return The deserialized value as a {@link String}.
     * @throws IOException
     */
    @Override
    public String deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {

        final TreeNode node = jsonParser.getCodec().readTree(jsonParser);

        final String unescapedString = StringEscapeUtils.unescapeJava(node.toString());
        return unescapedString.substring(1, unescapedString.length()-1);
    }
}

Annotate the field you want to deserialize like this:

@JsonDeserialize(using = JsonToStringDeserializer.class)

I initially followed advice that said to use a TreeNode like this:

    final TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser);
    return treeNode.toString();

But then you get a Json String that contains escape characters.

2

What you are trying to do is outside the scope of Jackson (and most, if not all other Java JSON libraries out there). What you want to do is fully consume the input stream into a string, then attempt to convert that string to a JSON object using Jackson. If the conversion fails then do something with the intermediate string, else proceed normally. Here's an example, which utilizes the excellent Apache Commons IO library, for convenience:

final InputStream stream ; // Your stream here

final String json = IOUtils.toString(stream);
try {
    final JsonNode node = new ObjectMapper().readTree(json);
    // Do something with JSON object here
} catch(final JsonProcessingException jpe) {
    // Do something with intermediate string here
}
Perception
  • 79,279
  • 19
  • 185
  • 195
  • I believe you are right about it being out of the scope. I was just hoping for some other solution as doing something like this kinda defeats the performance benefits of using the streaming parser. Your solution does however work. – cottonBallPaws May 31 '13 at 17:52
  • 1
    The streaming parser assumes that the incoming stream is valid (it kinda has to, since its operating in token push mode). For the very same performance reasons you mention it doesn't 'cache' stuff its handled already, it just checks to see if it has a valid token, and if it does it sends it on to the next handler in the chain. If it doesn't, it croaks. Another solution would be to implement and chain an accumulating stream on to whatever input stream your working with. Not necessarily clean but it could alleviate concerns you have on streaming performance. – Perception May 31 '13 at 18:10