- GeoJSON writes a polygon as
{"type":"Polygon","coordinates":[[[100.0,0.0],[101.0,0.0],[101.0,1.0],[100.0,1.0],[100.0,0.0]]]}:coordinatesis an array of rings, each ring an array of[lon, lat]points. A MultiPolygon'scoordinatesis an array of such polygons. Polygon.fromGeoJSON(String)in Lucene core turns such text intoPolygonobjects, instead of building them fromdouble[]coordinates, and declaresthrows ParseExceptionfor text it cannot use.
null or {} becomes Java null inside the parser; the not-an-array check then calls getClass() on it and throws NullPointerException before its ParseException exists. Select the still to open it at full size. Generated with AI, checked against the code.- Parse
{"type":"Polygon","coordinates":"x"}withPolygon.fromGeoJSON:coordinatesis a string, not an array. The parser's check rejects it:ParseException: coordinates should be an array, but got: class java.lang.String at character offset 32. - Now pass
{"type":"Polygon","coordinates":null}(or"coordinates":{}). You expect the sameParseException, the only exception the method declares. - Instead you get
NullPointerException: Cannot invoke "Object.getClass()" because "o" is null. The parser stores JSONnulland nested objects as Javanull, and the check builds its message with+ o.getClass()(line 208). - A MultiPolygon element fails the same way:
{"type":"MultiPolygon","coordinates":[{}]}throws at line 90. Acatch (ParseException e)misses both, while string, number and boolean values getParseException.
Applications that call Polygon.fromGeoJSON on GeoJSON they did not produce and catch only ParseException, when coordinates is null or an object such as {}: directly, in a Feature geometry, or as a MultiPolygon element, for example from a serializer that writes null for an unset field. Rare; the input is rejected either way, and nothing wrong is returned or indexed.
Mechanism, terms and numbers618 words
Polygon.fromGeoJSON(String) declares throws ParseException and is backed by the package-private SimpleGeoJSONPolygonParser. The parser checks that "coordinates" is an array, and for a MultiPolygon that each element of it is an array, and otherwise throws ParseException("coordinates should be an array, but got: " + o.getClass()) or "elements of coordinates array should be an array, but got: " + o.getClass(). But it stores a JSON null value, and every nested object it skips, as Java null. So for {"type":"Polygon","coordinates":null}, "coordinates":{} or {"type":"MultiPolygon","coordinates":[{}]}, building the message dereferences null and java.lang.NullPointerException: Cannot invoke "Object.getClass()" because "o" is null escapes instead of the ParseException; a catch (ParseException e) does not see it. A string, number or boolean in the same place gets the intended ParseException, and every other message in the class that reports a parsed value concatenates the value itself (+ o, + point, ...), which is null-safe. (An object whose own members trip one of the parser's checks, such as {"type":null}, gets that check's ParseException first.) The input is malformed and is rejected either way: there is no wrong polygon, hang or index effect, only the wrong exception. The two messages came with the parser in LUCENE-7380 (6.2.0, 2016) and are in every release since, including 10.6.0 and main. A null-safe type name in the two messages fixes it.
SimpleGeoJSONPolygonParser: the package-private parser behindPolygon.fromGeoJSON(Polygon.java:255-257). It "does minimal parsing of a GeoJSON object, to extract either Polygon or MultiPolygon" (SimpleGeoJSONPolygonParser.java:47-51).o: the Java value the parser builds for each JSON member value: aListfor an array,String,Double,Boolean, ornull.
- In
parseObject, a nested object is parsed recursively and then, if that parse throws nothing, dropped aso = null(:146-154); the literalnullalso giveso = null(:163-165). InparseArray, an object element is added to the list asnull(:310-313). - The
coordinatescheck (:205-209) isif (o instanceof List == false) { ... throw newParseException("coordinates should be an array, but got: " + o.getClass()); }.null instanceof Listis false, so anulloenters the branch, ando.getClass()(:208) throws before theParseExceptionis created. - The MultiPolygon element check in
parse()(:86-91) does the same with"elements of coordinates array should be an array, but got: " + o.getClass()(:90). - Every other message that reports a parsed value concatenates the value itself, which prints
nullsafely:+ oat:177,:182,:189,:231-232and:239-240;+ pointat:256;+ pointListand+ pointList.get(0|1)at:262,:267and:272.
Main 645d4fe14a20, Polygon.fromGeoJSON on four inputs (a small probe against the build's classes):
{"type":"Polygon","coordinates":"x"} -> ParseException: coordinates should be an array, but got: class java.lang.String at character offset 32; fragment leading to this:
{"type":"Polygon","coordinates":null} -> java.lang.NullPointerException: Cannot invoke "Object.getClass()" because "o" is null
{"type":"Polygon","coordinates":{}} -> java.lang.NullPointerException: Cannot invoke "Object.getClass()" because "o" is null
{"type":"MultiPolygon","coordinates":[{}]} -> java.lang.NullPointerException: Cannot invoke "Object.getClass()" because "o" is nullThe same through a Feature geometry with "coordinates":null and a FeatureCollection feature whose MultiPolygon coordinates is {} (an independent reproducer and a devil's-advocate survey). Not affected: type null or {}, Polygon coordinates [{}], [[{}]] or [null] (a literal null inside an array is rejected earlier by parseArray), MultiPolygon [[{}]], geometry null: all give ParseException. So does an object whose members fail the parser's own checks: "coordinates":{"type":null} gives ParseException: type should be a string, but got: null from the nested parse, while {"a":1} gives the NullPointerException.
Exposure and likelihood201 words
- Who is hit. Callers of
Polygon.fromGeoJSON(core;Polygonis@lucene.experimental) that handle bad input withcatch (ParseException e), the only exception the method declares. Nothing in Lucene calls the parser on its own. - What it takes. Malformed GeoJSON (RFC 7946 requires
coordinatesto be an array) whosecoordinatesmember isnullor an object the parser skips without error (such as{}). No report to the Lucene project since 6.2.0 (2016); likelihood "rare". - What happens. The unchecked
NullPointerExceptionbypasses theParseExceptionhandler, and its message says nothing about the input; on JDKs without helpful NullPointerException messages (JEP 358, on by default since JDK 15) it has no message at all. The recorded runs with assertions on and off behave the same. - What does not happen. No polygon is returned for the bad input, so nothing wrong is indexed or queried; no hang, no resource use, no state left behind (one parser object per call). Valid GeoJSON is not affected.
- Related, separate fix sites (not part of this bug). Empty Polygon coordinates
[]giveIndexOutOfBoundsExceptioninparsePolygon(:228, no check there); a bad\uescape givesNumberFormatException(part of LFV-569); deep nesting givesStackOverflowError(a separate finding).
One JUnit test on the public API with two one-line inputs: coordinates set to null, and a MultiPolygon whose only element is {}. Each must throw ParseException. Deterministic, single thread, no randomness; about a second including JVM start.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.geo;
import java.text.ParseException;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestGeoJSONNullCoordinates extends LuceneTestCase {
public void testNonArrayCoordinates() {
// expected ParseException "...should be an array", actual NPE from o.getClass() on null
expectThrows(
ParseException.class,
() -> Polygon.fromGeoJSON("{\"type\":\"Polygon\",\"coordinates\":null}"));
expectThrows(
ParseException.class,
() -> Polygon.fromGeoJSON("{\"type\":\"MultiPolygon\",\"coordinates\":[{}]}"));
}
}
cp TestGeoJSONNullCoordinates.java $LUCENE/lucene/core/src/test/org/apache/lucene/geo/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.geo.TestGeoJSONNullCoordinates -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
- Expected
ParseException: coordinates should be an array, but got: ..., as for a string value.- Observed
NullPointerException: Cannot invoke "Object.getClass()" because "o" is nullatSimpleGeoJSONPolygonParser.java:208; the same with assertions off.
More files, environment, full output209 words
The first line covers the coordinates message (:208), the second the MultiPolygon element message (:90). On a stock build the first expectThrows already fails, so the second is not reached; a scratch copy holding only the second line fails the same way at :90, and with the fix both pass.
As the regression test: the same test, for example as a new method in TestPolygon next to the testIllegalGeoJSON* tests.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); this test also fails on the 10.6.0 release; the same inputs fail on all 11 releases run from 6.2.0 to 10.6.0 (the rest inferred from unchanged code) |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
cp TestGeoJSONNullCoordinates.java $LUCENE/lucene/core/src/test/org/apache/lucene/geo/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.geo.TestGeoJSONNullCoordinates -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
# gradle, main 645d4fe14a20, seed DEADBEEF (assertions on; the same with -Ptests.asserts=false)
TestGeoJSONNullCoordinates > testNonArrayCoordinates FAILED
junit.framework.AssertionFailedError: Unexpected exception type, expected ParseException but got java.lang.NullPointerException: Cannot invoke "Object.getClass()" because "o" is null
at __randomizedtesting.SeedInfo.seed([DEADBEEF:B0DEAA79794FCE0A]:0)
at app//org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1437)
at app//org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1418)
at app//org.apache.lucene.geo.TestGeoJSONNullCoordinates.testNonArrayCoordinates(TestGeoJSONNullCoordinates.java:26)
Caused by:
java.lang.NullPointerException: Cannot invoke "Object.getClass()" because "o" is null
at org.apache.lucene.geo.SimpleGeoJSONPolygonParser.parseObject(SimpleGeoJSONPolygonParser.java:208)
at org.apache.lucene.geo.SimpleGeoJSONPolygonParser.parse(SimpleGeoJSONPolygonParser.java:65)
at org.apache.lucene.geo.Polygon.fromGeoJSON(Polygon.java:256)
at org.apache.lucene.geo.TestGeoJSONNullCoordinates.lambda$testNonArrayCoordinates$0(TestGeoJSONNullCoordinates.java:28)
# release jars 10.6.0 (assertions on and off): the same failure at SimpleGeoJSONPolygonParser.java:208- The check exists for exactly this input (
coordinatesis not an array, so throwParseException). It works for strings, numbers and booleans and fails only while building its message fornulland{}. - Every other message that reports a parsed value concatenates it (null-safe), and
TestPolygonexpectsParseExceptionfor malformed GeoJSON. - The introducing issue records "improved the ParseException throwing".
The full argument167 words
- The branch's only job is the
ParseException.SimpleGeoJSONPolygonParser.java:205-209and:86-91test "value is not aList" and do nothing but throwParseException.nulland objects are two of the five non-array JSON value kinds; for the other three the branch works. - Sibling messages.
:177,:182,:189,:231-232,:239-240,:256,:262,:267and:272concatenate the value (o,pointorpointList), which is null-safe, and those inputs getParseException. - Contract and tests.
Polygon.java:255declares onlythrows ParseException.TestPolygon.testIllegalGeoJSONExtraCrapAtEnd(:268-281),testIllegalGeoJSONLinkedCRS(:283-302) andtestIllegalGeoJSONMultipleFeatures(:305-346) expectParseException; no test expects aNullPointerException, and none asserts the two messages. - Intent. LUCENE-7380 (#8433): the parser "throws an exception otherwise" for inputs it cannot handle, and the author wrote before committing "I added class level javadocs, and improved the ParseException throwing".
git blameattributes both messages and theo = nullbranches to that commit (apart from a reformat).
The devil's-advocate check157 words
The strongest case for "intended" (an intended-behaviour checker, and a read-only reviewer from another model family, OpenAI GPT-5.6, which said "not real"): the inputs are not standard GeoJSON; the javadoc ("Parses a standard GeoJSON polygon string") has no @throws and promises no exception type for malformed input; Polygon is @lucene.experimental and the parser calls itself minimal; a throws clause never excludes unchecked exceptions; and the parser has other unchecked exits on malformed input, so "always ParseException" was never achieved.
Why it fails: the claim does not rest on an outside contract but on the parser's own check, which detects exactly this input and tries to throw ParseException; a slip in the message expression stops it. The refuting reviewer conceded that null "reaches a NullPointerException instead of the intended local ParseException branch" and agreed with the root cause. The other unchecked exits are places with no check at all. The argument limits severity (low), not validity.
Recommended A. A null-safe type name in both messages
Build the two messages with a null-safe type name: a 3-line helper typeName(o) returns "an object or null" for null, else o.getClass(), so string, number and boolean messages are unchanged. With it the repro passes with assertions on and off, and the 18 org.apache.lucene.geo test classes and :lucene:core:check pass.
- "elements of coordinates array should be an array, but got: " + o.getClass());
+ "elements of coordinates array should be an array, but got: " + typeName(o));
...
- throw newParseException("coordinates should be an array, but got: " + o.getClass());
+ throw newParseException("coordinates should be an array, but got: " + typeName(o));
...
+ /** Describes a parsed value for an error message; objects and JSON null are parsed as null. */
+ private static String typeName(Object o) {
+ return o == null ? "an object or null" : o.getClass().toString();
+ }All options and trade-offs3 options ยท 519 words
Two options were run through gradle in a scratch worktree of 645d4fe14a20; A is recommended. A fix conflicts textually with open PR #12295, which rewrites the same two checks but keeps getClass().
In SimpleGeoJSONPolygonParser (:90, :208), plus a small helper.
- "elements of coordinates array should be an array, but got: " + o.getClass());
+ "elements of coordinates array should be an array, but got: " + typeName(o));
...
- throw newParseException("coordinates should be an array, but got: " + o.getClass());
+ throw newParseException("coordinates should be an array, but got: " + typeName(o));
...
+ /** Describes a parsed value for an error message; objects and JSON null are parsed as null. */
+ private static String typeName(Object o) {
+ return o == null ? "an object or null" : o.getClass().toString();
+ }Trade-offs
- Messages for string, number and boolean values stay exactly as they are (
class java.lang.String, ...). nulland objects both read "but got: an object or null": the parser cannot tell them apart after parsing.- Error path only: no change to the public API signature, to successful parses or to performance; only the two messages change for
nulland object values.
What the checks validated
Gradle in a scratch worktree of 645d4fe14a20 (removed afterwards), seed DEADBEEF:
- the repro fails on the stock build and passes with the fix, assertions on and off;
TestPolygon16 of 16; all 18 classes oforg.apache.lucene.geoincl. the repro, 194 tests, 0 failures (7 skipped inTestTessellator);:lucene:core:check -x testwith the repro in place: BUILD SUCCESSFUL;- messages:
nulland{}giveParseException: coordinates should be an array, but got: an object or null at character offset 32; MultiPolygon[{}]giveselements of coordinates array should be an array, but got: an object or null at character offset 42;"x"still gives... but got: class java.lang.String ....
Replace + o.getClass() by + o at :90 and :208 (two one-token changes).
- throw newParseException("coordinates should be an array, but got: " + o.getClass());
+ throw newParseException("coordinates should be an array, but got: " + o);Trade-offs
The smallest diff and consistent with the rest of the class. But {} then reads "but got: null" (misleading for an object), and string, number and boolean messages change from the class name to the value (for example "but got: x"); no test asserts the old text.
What the checks validated
Same scratch worktree: the repro passes (assertions on) and TestPolygon 16 of 16. Messages: "x" gives but got: x; null, {} and MultiPolygon [{}] all give but got: null.
Have parseObject/parseArray keep a marker (for example an empty map) for nested objects so the messages can say "an object".
Trade-offs
Touches the value model that features arrays and parsePolygon rely on: more change than this bug needs. Not tried.
An application that validates client GeoJSON with Polygon.fromGeoJSON and catches only ParseException lets this NullPointerException escape its handler, so a bad request may surface as an internal error.
Non-formal: a randomized differential test, written to compare a fix for a sibling GeoJSON parser bug with the stock parser, saw both parsers throw NullPointerException on documents with "coordinates": {}. Reading the code found the two getClass() messages, and a public-API JUnit test reproduced it.
The method in full150 words
Method: non-formal (randomized differential testing, then code reading and executed tests). On 2026-10-28 the hunt unit for the GeoJSON and WKT parsers (siblings of #16423) was checking its fix for a per-level path-string memory bug in the same parser: a differential test ran random GeoJSON-like documents through the fixed parser and a renamed copy of the stock one. Both threw NullPointerException on documents with "coordinates": {}. That was not a difference between them, but it was a lead: reading SimpleGeoJSONPolygonParser found the two + o.getClass() messages and the o = null value branches. A 4-test public-API JUnit (null, {}, MultiPolygon [{}], a string control) reproduced it on the stock jar; independent checkers then reproduced it with their own tests, and it was cut to the one-test repro. The finder labelled it semi-formal; it is classed as non-formal because no model, proof or bounded-exhaustive check gave the signal.
Not reported to the Lucene project: no issue, PR, commit, list thread or downstream report (2026-10-28, refreshed 2026-11-01). But a public outside Rust port documents and imitates this exact NullPointerException as Java behaviour, without reporting it to the project.
Searches5 searches
The port that writes it down (read-only GitHub API, 2026-11-01). The public repository huyz0/lucene-rust, a Rust port of Lucene 10.5.0, added its port of this parser in commit 634835d7cb (2026-10-02; the API lists no PR for it). Its module comment says Java's o.getClass() on a null value (NullPointerException) escapes "as Java's runtime exceptions do"; its value type notes that JSON null and a nested object both become null; it returns the Java message Cannot invoke "Object.getClass()" because "o" is null for a null value; and a unit test asserts that {"type": "Polygon", "coordinates": null} gives that error. Its PR #33 (merged 2026-10-28, the day the hunt found the bug) shows the comment as diff context and edits the parity-table row for SimpleGeoJSONPolygonParser, which lists NullPointerException among the exceptions that "escape as Java's". The port copies the behaviour on purpose, calls it Java behaviour rather than a bug, and never reported it to Lucene. The hunt's first searches missed it (code search does not index the repository).
Other close items, none a report of this bug: #8433 (LUCENE-7380, the introducing issue; records the ParseException intent, no discussion of null or object coordinates); #12295 (open since 2023, "Use instanceof pattern-matching where possible": rewrites both checks, keeps + o.getClass(); a fix conflicts with it textually); #10007 (LUCENE-8964, an earlier fix for string arrays in the same parser, other site). An outside Go port's source comment notes only that the Java code "embeds Class.getClass()", without saying it throws; it is not a report to the project. The exact message texts give 0 issue or PR hits on apache/lucene; GitHub-wide, the full message "coordinates should be an array, but got" with GeoJSON gives 0, and the shorter phrase gives only unrelated token matches (88). Related findings in the same parser with other fix sites: LFV-562 and LFV-569. Limits: GitHub search matches tokens, not phrases; code search does not index every repository; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene and apache/lucene-solr issues and PRs (incl. the migrated JIRA issues) and commits | 2026-10-28 | two scripted prior-report searches (56 queries each, 239 and 281 distinct hits), the finder's (51) and a verifier's (37) runs, 44 manual issue/PR and 5 code searches: fromGeoJSON, SimpleGeoJSONPolygonParser, GeoJSON with NullPointerException, null, coordinates, MultiPolygon, ParseException, and the exact message texts (both on apache/lucene; GitHub-wide only the first, with and without GeoJSON) | no report; the message texts 0 hits on apache/lucene; closest #8433, #12295, #10007 |
| Open PRs | 2026-10-28 | changed files of all 414 open PRs, and every PR created 2026-10-26 to 2026-10-28 (#16907-#16936) | only #12295 touches the parser, and keeps getClass() |
| JIRA archive, mailing lists, downstream projects, ports, web | 2026-10-28 | all 13 LUCENE JIRA issues mentioning GeoJSON; dev@, java-user@, issues@ and commits@; Elasticsearch, OpenSearch and Solr; five outside ports or copies; 15 web queries | no report (the Rust port below was not among the five ports checked) |
| Upstream main and GitHub search | 2026-11-01 | the parser file at 2f424fd428da (2026-10-30) and its commit list; issue search fromGeoJSON, and GeoJSON items created since 2026-10-28 | both + o.getClass() lines still present; newest commit on the file #15681 (2026-02-11); fromGeoJSON finds only #8433; 0 new GeoJSON items |
| Outside Rust port huyz0/lucene-rust (public GitHub repository and its PR #33) | 2026-11-01 | read-only GitHub API: the repository, the commit list of its port of the parser, that file at its first commit 634835d7cb and at main, PR #33 and its parity doc | documents and imitates the exact NullPointerException as Java behaviour (comment, message and a unit test with "coordinates": null) since 2026-10-02; not a report to Lucene |
Born with the API: LUCENE-7380 (343f374b530, 2016-07-15) added fromGeoJSON and both messages; first release 6.2.0. Every release from 6.2.0 to 10.6.0 and main is affected (11 releases and main executed, the rest inferred from unchanged code); 6.1.0 has no fromGeoJSON.
Versions tested5 rows
Introduced by LUCENE-7380 "Add Polygon.fromGeoJSON" (#8433, by Mike McCandless after a +1 from Robert Muir on the JIRA patch; no GitHub PR): main-line commit 343f374b530 (first shipped in 7.0.0) and branch_6x backport 573aaf75f52 (shipped in 6.2.0), both 2016-07-15. git log -S'o.getClass()' on the file shows only this commit. Later commits to the file (LUCENE-8964 string arrays in parseArray, the LUCENE-9570 reformat that moved the lines from :201/:86 to :208/:90 in 9.0.0, typo and cosmetic changes up to #15681 in 2026) change no behaviour here. Not fixed upstream as of 2f424fd428da (2026-10-30).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal repro fails with assertions on and off (standalone and through Gradle); passes with fix A or B |
| 10.6.0 | affected | minimal repro fails with assertions on and off (release jars, checksums verified) |
branch_10_6 4ae946e5a9b, 10.5.2, 9.12.3 | affected | the candidate's JUnit fails 3 of 4 (the string control passes); the probe fails 3 of 3 |
| 6.2.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 10.0.0 | first affected 6.2.0 | probe: null, {} and MultiPolygon [{}] throw NullPointerException; the string control gets ParseException |
| 6.1.0 | no fromGeoJSON | the probe does not compile: the API does not exist yet |
Confirmed, medium confidence because the impact is small: the finder, an independent reproducer, an intended-behaviour devil's advocate, an adjudication and a credibility audit all reproduced it. A read-only reviewer from another model family called it "not real" on contract grounds but agreed with the root cause.
Checks and verdicts327 words
Everyone agrees on the root cause and that only the exception type is wrong: the input is rejected either way. Fact checks of this doc (2026-11-01): an independent one re-ran the repro, both fixes and the gradle command and found one prior-art wording error; a read-only cross-model one (OpenAI GPT-5.6) found the Rust-port overlap and some wording overstatements. All are corrected.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | 4-test JUnit fails 3 of 4 on main with assertions on and off, branch_10_6, 10.5.2 and 9.12.3; the string control passes; with the fix 4 of 4 and upstream TestPolygon 16 of 16. |
| cross-model review, intended lens (OpenAI GPT-5.6, read-only) | real, medium | Rejected the works-as-designed case because the non-array checks exist only to throw ParseException. A separate re-run: 3 of 4 fail. |
| independent reproducer | real | Its own JUnit outside the geo package (3 controls, 5 claim cases incl. {"a":1.0} and a Feature geometry) fails 5 of 8 on main (assertions on and off), 10.5.2 and 9.12.3; its own fix passes 8 of 8. |
| cross-model review, refute lens (OpenAI GPT-5.6, read-only) | not real, high | Could not run code; argued malformed input is outside the contract and the javadoc promises no exception type. Agreed with the root cause and conceded the NPE replaces the intended ParseException branch. Re-runs: 3 of 4 fail on main and 10.5.2. |
| intended-behaviour devil's advocate | real, low | Built the strongest intended case and rejected it; 11 sibling malformed inputs give ParseException; main, 10.5.2 and 9.12.3 fail 3 of 4. |
| adjudication | confirmed, medium | Re-ran the reproducer's JUnit with assertions on and off (5 of 8 fail each) and the finder's (3 of 4). It classed it as a wrong exception type on malformed GeoJSON. |
| Registration check and credibility audit | confirmed | Re-runs on main fail 3 of 4 with assertions on and off; the one-test repro fails on main and passes with the fix. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 407 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,437 words
- Registry title
- Polygon.fromGeoJSON throws NullPointerException instead of its own ParseException 'coordinates should be an array, but got: ...' when "coordinates" (or an element of a MultiPolygon's coordinates) is JSON null or an object, because the error message calls o.getClass() on the null the parser uses for those values (every release since 6.2.0)
- Area
- core / geo GeoJSON parsing (Polygon.fromGeoJSON, SimpleGeoJSONPolygonParser)
- Priority
- Low, rank 1074: An NPE instead of ParseException for malformed GeoJSON coordinates is a loud wrong-error on input that is rejected either way.
- Impact class
- wrong error
- Impact note
- Hits applications that call Polygon.fromGeoJSON (core; Polygon is @lucene.experimental; the method has no production caller in the Lucene tree) on malformed GeoJSON whose "coordinates" member is null or an object (for example {}), directly, inside a Feature's geometry, or as an element of a MultiPolygon's or a FeatureCollection feature's coordinates, and that rely on the declared ParseException to reject bad input: they get an unchecked java.lang.NullPointerException instead, which a catch (ParseException) handler does not see; the input is rejected either way (no polygon is returned), nothing hangs, no index is touched, no setting or scale is involved, with -ea and -da, in every release from 6.2.0 to 10.6.0 and on main.
- Summary
- You call Polygon.fromGeoJSON on a GeoJSON Polygon whose "coordinates" is null or an object, such as {"type":"Polygon","coordinates":null}, or on a MultiPolygon with an object element, {"type":"MultiPolygon","coordinates":[{}]}, and expect the ParseException that the parser has an explicit check for ('coordinates should be an array, but got: ...', 'elements of coordinates array should be an array, but got: ...'), as you get when coordinates is a string, a number or a boolean. Instead java.lang.NullPointerException ('Cannot invoke "Object.getClass()" because "o" is null') escapes, because SimpleGeoJSONPolygonParser stores JSON null and nested objects as Java null and both checks build their message with + o.getClass(). Every other type check in the class builds its message with + o, which is null-safe.
- Introduced
- 343f374b530fa71dc6102d74725b536f5f1367f3 (main line, 2016-07-15, 'LUCENE-7380: add Polygon.fromGeoJSON', by mikemccand after a +1 from rmuir on the JIRA patch; LUCENE-7380 is apache/lucene#8433; JIRA-patch era, no pull request; first shipped in 7.0.0) and its branch_6x backport 573aaf75f52f446c8b7ab915eefd420013c544a1 (same message, shipped in 6.2.0). The bug was born with the API and is not a regression [read: git blame and git log -L on main attribute both check lines, both getClass() messages and the null-producing value branches to 343f374b530, apart from the LUCENE-9570 reformat 2d6ad2fee6d (2020-12-23) that split the MultiPolygon message line; git log -S'o.getClass()' on the file shows only the introducing commit; exec: 6.1.0 has no fromGeoJSON (compile error), 6.2.0 fails] (2016-07-15 (author and commit date of 343f374b530 and 573aaf75f52))
- First affected version
- 6.2.0 [exec] (the first release with Polygon.fromGeoJSON and SimpleGeoJSONPolygonParser; 6.1.0 has no fromGeoJSON [exec: compile error]); first 7.x release 7.0.0 [exec], from the main-line commit
- Affected versions
- Every release that has Polygon.fromGeoJSON: 6.2.0-6.6.6, all 7.x, all 8.x through 8.11.4, 9.0.0-9.12.3 and 10.0.0-10.6.0 (10.6.0 run as its release revision, branch_10_6 head 4ae946e5a9b9), plus main a9779ef3c193. [exec] the history probe (three claim inputs plus a string-valued control) fails 3 of 3 with the control passing on 6.2.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, branch_10_6 and main (-ea and -da); the finder's JUnit fails 3 of 4 (control passes) on main (-ea and -da), branch_10_6, 10.5.2 and 9.12.3 (-da); the independent reproduction's own JUnit fails 5 of 8 (3 controls pass) on main (-ea and -da), 10.5.2 and 9.12.3; the intended-behaviour check on main (-ea and -da), 10.5.2 and 9.12.3 (-da); the adjudication on main (-ea and -da); the registration check on main (-da, seed 5EED0384, 3 of 4). [read] the other releases are inferred affected because the check lines and the null-producing lines are unchanged in behaviour from 343f374b530 to main. Not fixed upstream: upstream main 645d4fe14a20 (author date 2026-10-28) still has '+ o.getClass()' at SimpleGeoJSONPolygonParser.java:90 and :208, and its newest commit on the file is 448c7d8e241 (author date 2026-02-11, an unrelated toArray cleanup). All runs on Linux aarch64 with JDK 25.0.3; on JDKs before 15 (unless enabled on 14) the exception type is the same but the NullPointerException has no helpful message.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that disputed it (the adjudication ruled it real), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- non-formal; also reached by code reading
- Upstream and prior reports
- none found (searched 2026-10-28, read-only): two scripted prior-report searches (56 queries each over GitHub issues/PRs/commits, Elasticsearch/OpenSearch/Solr, dev@, java-user@, issues@ and commits@, the JIRA archive and the web; 239 and 281 distinct hits, 1 high = #8433, the introducing issue) plus the finder's run (51 queries, 1,382 hits, 1 high = #9568, an unrelated Geo3D NullPointerException) and the intended-behaviour check's (37 queries, 92 hits, 1 high = #8433); manual GitHub searches (fromGeoJSON, SimpleGeoJSONPolygonParser, GeoJSON plus NullPointerException/null/coordinates/MultiPolygon/ParseException, the exact message texts '"coordinates should be an array"' and '"elements of coordinates array"' (0 issue or PR hits on apache/lucene; GitHub-wide, the full first message with GeoJSON gives 0 and the shorter phrase only unrelated token matches), apache/lucene-solr, GitHub-wide and in Elasticsearch, OpenSearch and Solr), every LUCENE JIRA ticket mentioning GeoJSON (13), list searches, 15 web queries, five outside ports or copies of the parser, and the changed files of all 414 open apache/lucene PRs: nothing reports or discusses the NullPointerException or null/object coordinates. Closest items, not reports: #8433 (LUCENE-7380, the introducing issue: 'improved the ParseException throwing', no null/object discussion); #12295 (open since 2023-05, the only open PR touching the parser: rewrites both instanceof checks but keeps '+ o.getClass()' in both messages, so it does not fix this and a fix would conflict textually with it); #10007 (LUCENE-8964, 8.3.0, string arrays in properties, other site). An outside Go port's comment only notes that 'the Java reference embeds Class.getClass()' and substitutes a nil-safe name, without saying the Java code throws; a Kotlin port and a verbatim copy carry the defect without comment. Refresh 2026-10-28 (GitHub search and REST API, read-only): 'fromGeoJSON' finds only #8433; 'SimpleGeoJSONPolygonParser' finds only #9584 and #9498 (unrelated); '"coordinates should be an array"' 0; 'GeoJSON updated:>=2026-10-20' 0; none of the 20 apache/lucene PRs created since 2026-10-26 (#16907-#16936) touches a geo file; upstream main 645d4fe14a20 still has both getClass() messages and no commit on the file after 448c7d8e241 (author date 2026-02-11). Found on 2026-11-01 (read-only GitHub API): the public outside Rust port huyz0/lucene-rust documents and imitates this NullPointerException as Java behaviour (a module comment, the Java message and a unit test with "coordinates": null, since its commit 634835d7cb of 2026-10-02; shown as diff context in its PR #33, merged 2026-10-28); it is not a report to the Lucene project.
- Lucene commit tested
- main
645d4fe14a20(the minimal repro with assertions on and off, standalone and through gradle; both fix options in a scratch worktree); the 10.6.0 release jars (checksums verified against Maven Central; the minimal repro with assertions on and off); earlier: maina9779ef3c193, branch_10_64ae946e5a9b, 10.5.2 and 9.12.3 (the candidate's and an independent JUnit) and a probe on 10 releases from 6.2.0 to 10.5.2; 6.1.0 has nofromGeoJSON. Upstream main2f424fd428da(2026-10-30) still has both lines (read).
- Finding record
lucene/core/src/java/org/apache/lucene/geo/SimpleGeoJSONPolygonParser.java:47-51 (at a9779ef3c193) ('Does minimal parsing'), :84-91 (MultiPolygon element check, '+ o.getClass()' at :90), :146-154 (nested object value: parseObject then o = null), :163-165 (JSON null: o = null), :177, :182, :189, :232, :240 (sibling messages built with '+ o'), :205-209 (coordinates check, '+ o.getClass()' at :208), :310-313 (object element in an array: o = null)lucene/core/src/java/org/apache/lucene/geo/Polygon.java:41 (at a9779ef3c193) (@lucene.experimental), :248-257 (fromGeoJSON javadoc, no @throws; signature :255 declares ParseException); SECURITY.md lines 28, 42, 46, 50
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/geo/SimpleGeoJSONPolygonParser.java:25-51, :63-97, :132-172, :177, :182, :189, :205-209, :231-232, :239-240, :256, :310-313, :451-462 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/geo/Polygon.java:23-27, :41, :248-257 (at 645d4fe14a20)
- Who is affected
lucene/core/src/java/org/apache/lucene/geo/Polygon.java:41, :255-257 (at 645d4fe14a20); grep of lucene/ for fromGeoJSON and SimpleGeoJSONPolygonParser (only Polygon.java and tests)https://openjdk.org/jeps/358 (Helpful NullPointerExceptions)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/geo/SimpleGeoJSONPolygonParser.java:47-51, :86-91, :177, :182, :189, :205-209, :231-232, :239-240, :256, :262, :267, :272 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/geo/Polygon.java:41, :248-255 (at 645d4fe14a20)lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java:268-281, :283-302, :305-346 (at 645d4fe14a20)
- TestGeoJSONNullCoordinates.java (JUnit, one test):
repro/LFV-953/src/org/apache/lucene/geo/TestGeoJSONNullCoordinates.java(33 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the repro's inputs and outputs.
assets/LFV-953/bug.png(407 KB)
