Skip to content

Negative array index on set/delete/map throws a raw IndexOutOfBoundsException while read supports it #1077

Description

@haskiindahouse

Version / environment

  • json-path master @ HEAD (line numbers verified against source); latest release json-path 3.0.0 hits the same code path.
  • JVM: any (Java 8+). JSON provider: default (json-smart); reproduced cross-provider.

Description

JsonPath.read(json, "$[-1]") returns the last element. The update operations set, delete, and map with the same $[-1] throw a raw java.lang.IndexOutOfBoundsException that escapes the library. Read and write disagree on what $[-1] means, and callers get a JDK exception instead of the library's own PathNotFoundException.

Steps to reproduce

import com.jayway.jsonpath.*;

String json = "[\"a\",\"b\",\"c\"]";

// READ with negative index, returns "c"
Object last = JsonPath.read(json, "$[-1]");
System.out.println(last); // c

// SET with the same negative index, throws
DocumentContext ctx = JsonPath.parse(json);
ctx.set("$[-1]", "z");    // java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 3

// delete("$[-1]") and map("$[-1]", ...) throw the same way

Observed output

READ $[-1]: c
SET threw:    java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 3
DELETE threw: java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 3
MAP threw:    java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 3

Expected

set/delete/map on $[-1] update the last element, matching how read resolves $[-1]. Failing that, they raise PathNotFoundException, the library's own type, instead of leaking a raw IndexOutOfBoundsException from the provider.

Root cause

json-path/src/main/java/com/jayway/jsonpath/internal/path/PathToken.java, handleArrayIndex (line 136):

136:  PathRef pathRef = ctx.forUpdate() ? PathRef.create(model, index) : PathRef.NO_OP;
137:  int effectiveIndex = index < 0 ? ctx.jsonProvider().length(model) + index : index;
138:  try {
139:      Object evalHit = ctx.jsonProvider().getArrayIndex(model, effectiveIndex);
...
145:  } catch (IndexOutOfBoundsException e) {   // guards the READ only
146:  }

Line 136 builds the update PathRef from the raw index. Line 137 normalizes the negative index into effectiveIndex for the read at line 139, and the read sits inside the IOOBE catch at line 145. The update runs later with the raw negative index, reaches the provider, and throws outside any guard.

Suggested fix

Normalize the negative index before building the update PathRef, so update and read agree:

int effectiveIndex = index < 0 ? ctx.jsonProvider().length(model) + index : index;
PathRef pathRef = ctx.forUpdate() ? PathRef.create(model, effectiveIndex) : PathRef.NO_OP;

Convert an out-of-range update into PathNotFoundException rather than letting IndexOutOfBoundsException escape.


Found via property-based & differential bug-hunting, part of an effort to scale PBT (DepTyCheck-based) testing across the OSS ecosystem.

If this is intended / by-design: I'm really sorry, please just close it — no need to flag or ban me. I'm trying to scale property-based testing across the whole ecosystem and my publishing agents may have gotten this one wrong. I read every issue and follow up on each.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions