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;
}
}