Skip to content
Advertisement

Get the line number of a JSON file given a JSON Path/JSON Pointer in Java

I am looking for a way to parse a JSON file for a specific node and get that node’s line number in the file. I would like to use the Jayway JSONPath library to support extended JSONPath queries.

For example (from jsonpath.com), here’s some JSON:

{
  "firstName": "John",
  "lastName" : "doe",
  "age"      : 26,
  "address"  : {
    "streetAddress": "naist street",
    "city"         : "Nara",
    "postalCode"   : "630-0192"
  },
  "phoneNumbers": [
    {
      "type"  : "iPhone",
      "number": "0123-4567-8888"
    },
    {
      "type"  : "home",
      "number": "0123-4567-8910"
    }
  ]
}

and here’s a jsonPath: $.phoneNumbers.[?(@.type=='iPhone')]

I would like to have a way to say that this node is on line 11 in the json file. I don’t know ahead of time what the json contents might be or the jsonPath. Both are dynamic.

So far, I’ve tried to parse the json into a tree and traverse it up to the node to get the parser’s current location, but the parser must always run to the end of the file before the jsonPath executes. Any other ideas?

Answer

I eventually found a solution that involves using Jackson’s JsonFactory and JsonParser. It’s kludge-y to say the least, but it uses the JsonParser’s knowledge of its parser’s line number to get the JsonNode’s position and works pretty well.

I’ll paste the code here, but the code is also available at watchtower github

Calling class:

void findLineNumber() throws Exception{
    CustomParserFactory customParserFactory = new CustomParserFactory();
    ObjectMapper om = new ObjectMapper(customParserFactory);
    factory = new CustomJsonNodeFactory(om.getDeserializationConfig().getNodeFactory(),
            customParserFactory);
    om.setConfig(om.getDeserializationConfig().with(factory));
    config = Configuration.builder()
            .mappingProvider(new JacksonMappingProvider(om))
            .jsonProvider(new JacksonJsonNodeJsonProvider(om))
            .options(Option.ALWAYS_RETURN_LIST)
            .build();

    File filePath = ...;
    JsonPath jsonPath = ...;
    DocumentContext parsedDocument = JsonPath.parse(filePath, config);
    ArrayNode findings = parsedDocument.read(jsonPath);
    for (JsonNode finding : findings) {
        JsonLocation location = factory.getLocationForNode(finding);
        int lineNum = location.getLineNr();
        //Do something with lineNum
    }
}

CustomJsonNodeFactory.java

public class CustomJsonNodeFactory extends JsonNodeFactory {

    private static final long serialVersionUID = 8807395553661461181L;

    private final JsonNodeFactory delegate;
    private final CustomParserFactory parserFactory;

    /*
     * "Why isn't this a map?" you might be wondering. Well, when the nodes are created, they're all
     * empty and a node's hashCode is based on its children. So if you use a map and put the node
     * in, then the node's hashCode is based on no children, then when you lookup your node, it is
     * *with* children, so the hashcodes are different. Instead of all of this, you have to iterate
     * through a listing and find their matches once the objects have been populated, which is only
     * after the document has been completely parsed
     */
    private List<Entry<JsonNode, JsonLocation>> locationMapping;

    public CustomJsonNodeFactory(JsonNodeFactory nodeFactory,
        CustomParserFactory parserFactory) {
        delegate = nodeFactory;
        this.parserFactory = parserFactory;
        locationMapping = new ArrayList<>();
    }

    /**
     * Given a node, find its location, or null if it wasn't found
     * 
     * @param jsonNode the node to search for
     * @return the location of the node or null if not found
     */
    public JsonLocation getLocationForNode(JsonNode jsonNode) {
        return this.locationMapping.stream().filter(e -> e.getKey().equals(jsonNode))
                .map(e -> e.getValue()).findAny().orElse(null);
    }

    /**
     * Simple interceptor to mark the node in the lookup list and return it back
     * 
     * @param <T>  the type of the JsonNode
     * @param node the node itself
     * @return the node itself, having marked its location
     */
    private <T extends JsonNode> T markNode(T node) {
        JsonLocation loc = parserFactory.getParser().getCurrentLocation();
        locationMapping.add(new SimpleEntry<>(node, loc));
        return node;
    }

    @Override
    public BooleanNode booleanNode(boolean v) {
        return markNode(delegate.booleanNode(v));
    }

    @Override
    public NullNode nullNode() {
        return markNode(delegate.nullNode());
    }

    @Override
    public NumericNode numberNode(byte v) {
        return markNode(delegate.numberNode(v));
    }

    @Override
    public ValueNode numberNode(Byte value) {
        return markNode(delegate.numberNode(value));
    }

    @Override
    public NumericNode numberNode(short v) {
        return markNode(delegate.numberNode(v));
    }

    @Override
    public ValueNode numberNode(Short value) {
        return markNode(delegate.numberNode(value));
    }

    @Override
    public NumericNode numberNode(int v) {
        return markNode(delegate.numberNode(v));
    }

    @Override
    public ValueNode numberNode(Integer value) {
        return markNode(delegate.numberNode(value));
    }

    @Override
    public NumericNode numberNode(long v) {
        return markNode(delegate.numberNode(v));
    }

    @Override
    public ValueNode numberNode(Long value) {
        return markNode(delegate.numberNode(value));
    }

    @Override
    public ValueNode numberNode(BigInteger v) {
        return markNode(delegate.numberNode(v));
    }

    @Override
    public NumericNode numberNode(float v) {
        return markNode(delegate.numberNode(v));
    }

    @Override
    public ValueNode numberNode(Float value) {
        return markNode(delegate.numberNode(value));
    }

    @Override
    public NumericNode numberNode(double v) {
        return markNode(delegate.numberNode(v));
    }

    @Override
    public ValueNode numberNode(Double value) {
        return markNode(delegate.numberNode(value));
    }

    @Override
    public ValueNode numberNode(BigDecimal v) {
        return markNode(delegate.numberNode(v));
    }

    @Override
    public TextNode textNode(String text) {
        return markNode(delegate.textNode(text));
    }

    @Override
    public BinaryNode binaryNode(byte[] data) {
        return markNode(delegate.binaryNode(data));
    }

    @Override
    public BinaryNode binaryNode(byte[] data, int offset, int length) {
        return markNode(delegate.binaryNode(data, offset, length));
    }

    @Override
    public ValueNode pojoNode(Object pojo) {
        return markNode(delegate.pojoNode(pojo));
    }

    @Override
    public ValueNode rawValueNode(RawValue value) {
        return markNode(delegate.rawValueNode(value));
    }

    @Override
    public ArrayNode arrayNode() {
        return markNode(delegate.arrayNode());
    }

    @Override
    public ArrayNode arrayNode(int capacity) {
        return markNode(delegate.arrayNode(capacity));
    }

    @Override
    public ObjectNode objectNode() {
        return markNode(delegate.objectNode());
    }

}

CustomParserFactory.java (Note that this removes thread-safety, which can be kind of a big deal):

public class CustomParserFactory extends JsonFactory {

    private static final long serialVersionUID = -7523974986510864179L;
    private JsonParser parser;

    public JsonParser getParser() {
        return this.parser;
    }

    @Override
    public JsonParser createParser(Reader r) throws IOException, JsonParseException {
        parser = super.createParser(r);
        return parser;
    }

    @Override
    public JsonParser createParser(String content) throws IOException, JsonParseException {
        parser = super.createParser(content);
        return parser;
    }    
}
Advertisement