- A GeoJSON Feature wraps one geometry and free-form data:
{"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [...]}, "properties": {...}}. The"type"member names the kind of GeoJSON object;propertiesis any JSON object the producer chooses. Polygon.fromGeoJSON(String)in Lucene core turns a Polygon or MultiPolygon, bare, inside a Feature, or inside a one-Feature FeatureCollection, intoPolygon[], and throwsParseExceptionfor anything else (a Point, several features, a non-CRS84crs).
"type" key as the type of a GeoJSON object, inside properties too, while it checks "coordinates" only at geometry positions; so "type": "x" in properties aborts the parse of a valid Feature. Select the still to open it at full size. Generated with AI, checked against the code.- Parse a triangle with
Polygon.fromGeoJSON:{"type": "Polygon", "coordinates": [[[0, 0], [1, 0], [1, 1], [0, 0]]]}gives 1 polygon. Wrapped in a Feature with"properties": {"name": "park"}, it gives the same polygon. - Change the property to
"properties": {"type": "x"}. GeoJSON letspropertiesbe any JSON object, and the javadoc accepts a Polygon "optionally embedded under a "type: Feature"", so you expect the same 1 polygon. - Instead you get
ParseException: can only handle type FeatureCollection (...), Feature, Polygon or MultiPolygon, but got x at character offset 127. The parser checks every"type"key outsidecrsas a GeoJSON object type (line 186), soxlooks like an unsupported geometry. - Its sibling key
"coordinates"is read only at geometry positions, so{"coordinates": [...]}inpropertiesis ignored: only"type"is fatal. Data shaped like a geocoder's documented RFC 7946 output (with its full-geometry option), a one-Feature FeatureCollection with properties{"category": "boundary", "type": "administrative"}, fails with... but got administrative.
Applications that pass whole GeoJSON Features, not bare geometries, to Polygon.fromGeoJSON when properties hold a "type" key, such as data shaped like a geocoder's documented output, or copied map tags. No setting and no scale: one key in one document, in every release since 6.2.0. It fails loudly and never returns a wrong polygon; Lucene itself never calls the method.
Mechanism, terms and numbers597 words
Polygon.fromGeoJSON(String) (core, @lucene.experimental) accepts a Polygon or MultiPolygon "optionally embedded under a "type: Feature"", and GeoJSON lets a Feature's properties be any JSON object. The parser behind it, SimpleGeoJSONPolygonParser, tracks where each object sits (geometry, properties, features.[], ...) and reads "coordinates" only at geometry positions, but it reads a member named "type" as a GeoJSON object type at every position except under crs (SimpleGeoJSONPolygonParser.java:186). So a Feature whose properties hold {"type": "x"} is rejected with ParseException: can only handle type FeatureCollection (if it has a single polygon geometry), Feature, Polygon or MultiPolygon, but got x, and {"type": 3} with type should be a string, but got: 3.0. The same Feature without that key parses. Nothing wrong is ever returned: the error is loud and the input is valid. The check came with the parser in LUCENE-7380 (6.2.0, 2016); all 90 releases from 6.2.0 to 10.6.0 and main fail the one-test repro. Reading "type" only where a GeoJSON object can appear fixes it; the recommended fix also keeps the crs checks for a crs on a nested geometry.
SimpleGeoJSONPolygonParser: the package-private hand-written 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).- Path: the parser's address of the object it is reading, built from member names:
""for the top level,geometry,properties,properties.source,features.[],features.[].geometry. - Geometry path:
isValidGeometryPath(:221-224), the three places a Polygon may sit:"",geometry,features.[].geometry.
parseObjectparses a member whose value is an object recursively, with pathpath + "." + key(:146-153); so the Feature'spropertiesobject is read with pathproperties.- After each member, line 186 asks
if (key.equals("type") && path.startsWith("crs") == false): every"type"member outsidecrsis read as a GeoJSON object type, wherever it sits. - Inside that branch, a non-string value throws
type should be a string(:187-190);PolygonandMultiPolygonare accepted only on a geometry path (:192-195);FeatureandFeatureCollectiononly at""orfeatures.[](:196-197); anything else throwscan only handle type ... but got <value>(:199-204). At pathpropertiesno value can pass. - The next check,
key.equals("coordinates") && isValidGeometryPath(path)(:205), is scoped by path. That asymmetry is the bug:"coordinates"inpropertiesis ignored,"type"inpropertiesis fatal.
Main 645d4fe14a20, Polygon.fromGeoJSON on a one-triangle Polygon wrapped in different Features (a small probe against the build's classes; messages cut before "; fragment"):
Feature, properties {"name": "park"} -> OK, 1 polygon
Feature, properties {"type": "x"} -> ParseException: can only handle type ... but got x at character offset 127
Feature, properties {"type": 3} -> ParseException: type should be a string, but got: 3.0 at character offset 127
Feature, properties {"source": {"type": "survey"}} -> ParseException: ... but got survey at character offset 138
FeatureCollection of 1 Feature, properties {"category": "boundary", "type": "administrative"}
-> ParseException: ... but got administrative at character offset 194
Feature, properties = a decoy Polygon object -> ParseException: ... but got Polygon (the decoy is never used)
Feature with foreign member "centerline": {"type": "LineString", ...}
-> ParseException: ... but got LineString at character offset 145Also rejected (study verifiers and the tie-breaker): properties written before geometry, and {"parts": [{"type": "x"}]}, a "type" inside an object inside an array. Not affected: properties without a "type" key, a "coordinates" key in properties, a top-level crs.
Exposure and likelihood299 words
- Who is hit. Callers of
Polygon.fromGeoJSON(core;Polygonis@lucene.experimental,Polygon.java:41) that pass Features or one-Feature FeatureCollections whoseproperties(or an object nested in them) hold a key named"type". Default and only behaviour; assertions on or off make no difference. - Realistic trigger.
"type"is an ordinary data name. The output docs of the OpenStreetMap geocoder Nominatim say its GeoJSON format "follows the RFC7946" and list among thepropertiesfields "category, type - key and value of the main OSM tag". Data shaped like that, a one-Feature FeatureCollection with a polygon geometry and properties{"category": "boundary", "type": "administrative"}, is rejected on main and on 10.6.0; it was built from the docs, not fetched from the service. Caveats: the docs give the centroid (a Point, rejected anyway) unless the caller asks for the full geometry withpolygon_geojson, and the parser accepts a FeatureCollection only with a single polygon. In OpenStreetMap a multipolygon relation carries the tagtype=multipolygon; data that copies such tags intopropertiesis rejected the same way (... but got multipolygon). - Reach. In the Lucene tree the method has no production caller, only
TestPolygonandTestTessellator. A read-only code search found a benchmark call in Elasticsearch and none in OpenSearch (Solr not checked). No relevant public report was found in the recorded searches: likelihood "plausible", priority low. - What happens. The query area or document cannot be turned into a polygon, and the message is misleading: "but got administrative" names a property value, not a geometry type. Workaround: pass only the
geometryobject, or droppropertiesbefore calling. - What does not happen. No wrong or different polygon (a decoy geometry in
propertiesis rejected, never used), no hang, no extra cost beyond a normal parse, nothing written to an index.
One JUnit test on the public API: a Feature around a one-triangle Polygon with "properties": {"type": "x"} must parse to the same polygon as the bare geometry. Deterministic, single thread, no randomness; a few seconds 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 TestGeoJSONFeatureTypeProperty extends LuceneTestCase {
public void testTypeMemberInFeatureProperties() throws Exception {
String polygon = "{\"type\": \"Polygon\", \"coordinates\": [[[0, 0], [1, 0], [1, 1], [0, 0]]]}";
String feature =
"{\"type\": \"Feature\", \"geometry\": " + polygon + ", \"properties\": {\"type\": \"x\"}}";
// expected: the Feature parses to the bare polygon; actual: ParseException "... but got x"
try {
assertArrayEquals(Polygon.fromGeoJSON(polygon), Polygon.fromGeoJSON(feature));
} catch (ParseException e) {
throw new AssertionError("expected the Feature's polygon, got " + e, e);
}
}
}
cp TestGeoJSONFeatureTypeProperty.java $LUCENE/lucene/core/src/test/org/apache/lucene/geo/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.geo.TestGeoJSONFeatureTypeProperty -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
- Expected
- The same
Polygon[](1 polygon) as for the bare Polygon. - Observed
ParseException: can only handle type FeatureCollection (...), Feature, Polygon or MultiPolygon, but got x at character offset 127fromSimpleGeoJSONPolygonParser.java:201; the same with assertions off.
More files, environment, full output227 words
The ParseException is rethrown as an AssertionError so the failure states expected versus actual. The same test passes with either fix below, with assertions on and off.
As the regression test: the same test, for example as a new method in TestPolygon next to testGeoJSONPolygonFeature.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); the same test fails on the 10.6.0 and 10.5.2 releases and on every release from 6.2.0 |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
cp TestGeoJSONFeatureTypeProperty.java $LUCENE/lucene/core/src/test/org/apache/lucene/geo/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.geo.TestGeoJSONFeatureTypeProperty -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
# gradle, main 645d4fe14a20, seed DEADBEEF (assertions on; the same with -Ptests.asserts=false)
TestGeoJSONFeatureTypeProperty > testTypeMemberInFeatureProperties FAILED
java.lang.AssertionError: expected the Feature's polygon, got java.text.ParseException: can only handle type FeatureCollection (if it has a single polygon geometry), Feature, Polygon or MultiPolygon, but got x at character offset 127; fragment leading to this:
...[1, 0], [1, 1], [0, 0]]]}, "properties": {"type": "
at __randomizedtesting.SeedInfo.seed([DEADBEEF:8D822687064FAAE3]:0)
at org.apache.lucene.geo.TestGeoJSONFeatureTypeProperty.testTypeMemberInFeatureProperties(TestGeoJSONFeatureTypeProperty.java:32)
Caused by:
java.text.ParseException: can only handle type FeatureCollection (if it has a single polygon geometry), Feature, Polygon or MultiPolygon, but got x at character offset 127; fragment leading to this:
...[1, 0], [1, 1], [0, 0]]]}, "properties": {"type": "
at org.apache.lucene.geo.SimpleGeoJSONPolygonParser.newParseException(SimpleGeoJSONPolygonParser.java:459)
at org.apache.lucene.geo.SimpleGeoJSONPolygonParser.parseObject(SimpleGeoJSONPolygonParser.java:201)
at org.apache.lucene.geo.SimpleGeoJSONPolygonParser.parseObject(SimpleGeoJSONPolygonParser.java:153)
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.TestGeoJSONFeatureTypeProperty.testTypeMemberInFeatureProperties(TestGeoJSONFeatureTypeProperty.java:30)
# release jars 10.6.0 (assertions on and off): the same failure, newParseException at line 461- The javadoc accepts a Polygon "optionally embedded under a "type: Feature"" and links the GeoJSON spec, where
propertiesis "any JSON object". - The parser already reads
"coordinates"only at geometry positions and exemptscrsfrom the"type"check;propertieswas not exempted. - No test asserts this rejection; LUCENE-8964 already fixed this parser for rejecting other legal
propertiescontent.
The full argument287 words
- Contract.
Polygon.java:248-254: "Parses a standard GeoJSON polygon string. The type of the incoming GeoJSON object must be a Polygon or MultiPolygon, optionally embedded under a "type: Feature"", with a link to the 2008 GeoJSON specification. Its section 2.2: "The value of the properties member is an object (any JSON object or a JSON null value)"; RFC 7946 section 3.2 says the same. The 6.2.0 CHANGES entry promises parsing of "a standard GeoJSON string" (lucene/CHANGES.txt:8978-8980). - The parser's example and tests. The header comment (
SimpleGeoJSONPolygonParser.java:23-45) shows an accepted Feature with"properties": {"prop0": "value0", "prop1": {"this": "that"}};TestPolygon.testGeoJSONPolygonFeature(TestPolygon.java:176-199) parses it. - Scoping exists next door.
"coordinates"is read only on geometry paths (:205), andcrsis exempt from the"type"check (:186) because a crs object has its own"type"(testIllegalGeoJSONLinkedCRSeven has"type": "proj4"undercrs.properties,TestPolygon.java:283-303). The authors knew"type"occurs outside GeoJSON objects. - No test asserts it. The tested rejections concern the geometry, the feature count, the crs or trailing data:
testIllegalGeoJSONMultipleFeatures(:305-346, "but got Point" atfeatures.[].geometry),testIllegalGeoJSONLinkedCRS(:283-303),testIllegalGeoJSONExtraCrapAtEnd(:268-281). All 16 pass with the fix. - Intent on record. LUCENE-7380 (#8433): "The parser only handles the various ways that a single Polygon or MultiPolygon can appear in a GeoJSON string, and throws an exception otherwise." A Feature with a Polygon geometry is one of those ways; the issue never discusses
"type"keys in properties. - Precedent. LUCENE-8964 (#10007, 8.3.0,
lucene/CHANGES.txt:6239-6240"Fix geojson shape parsing on string arrays in properties"): the same parser rejected other legalpropertiescontent and the project fixed it, inparseArray; this bug reproduces on every release after that fix.
The devil's-advocate check195 words
The strongest case for "intended" (the study's intended-behaviour checker, the cross-model review lenses and the adjudication): the parser calls itself "minimal parsing" (:47-51), Polygon is @lucene.experimental, the parser deliberately rejects Point, LineString, GeometryCollection, several features and linked crs, and RFC 7946 section 6.1 says support for foreign members "can vary across implementations".
Why it fails: "minimal parsing" describes what is extracted, not a restriction on properties, and @lucene.experimental is about API stability. The deliberate rejections cover unsupported geometry types, more than one polygon, crs handling and trailing input; none concerns a "type" key in Feature properties, while the rejected input is exactly the documented shape. properties is a core Feature member, not a foreign member, so section 6.1 does not apply to the main case. For foreign members it cuts both ways: support "can vary", but the same section says "GeoJSON semantics do not apply to foreign members and their descendants, regardless of their names and values". The cross-model lenses rated the intended argument weak; the later cross-model fact check rated it moderate (a deliberately narrow parser), with a strong rebuttal for the properties case. Verdict: a defect, low severity.
Recommended B. Scope "type" to GeoJSON object positions and check every object's crs
Read "type" only where a GeoJSON object can appear (the top level, a geometry, a feature), and run the existing crs checks for a crs on any such object. With it the repro passes with assertions on and off, a nested non-CRS84 crs is still rejected, and TestPolygon, the org.apache.lucene.geo tests and :lucene:core:check pass.
All options and trade-offs3 options · 682 words
Two options were run through gradle in a scratch worktree of 645d4fe14a20; B is recommended. Both leave one residual: a foreign member literally named "geometry" (or "features") is still read as a GeoJSON location, but RFC 7946 section 7.1 forbids such members on a Geometry object, so only non-conforming input hits it. A fix conflicts textually with PR #12295 (open as of the 2026-11-01 scan).
B. Scope "type" to GeoJSON object positions and check every object's crs (validated by the checks) (recommended)
In SimpleGeoJSONPolygonParser: line 186 tests isGeoJSONObjectPath(path) (a geometry path or features.[]) instead of path.startsWith("crs") == false, and the two crs checks (:125, :174) run at the crs.properties of any GeoJSON object, not only the top level.
- if (path.equals("crs.properties") && key.equals("href")) {
+ if (isCrsPropertiesPath(path) && key.equals("href")) {
...
- if (path.equals("crs.properties") && key.equals("name")) {
+ if (isCrsPropertiesPath(path) && key.equals("name")) {
...
- if (key.equals("type") && path.startsWith("crs") == false) {
+ if (key.equals("type") && isGeoJSONObjectPath(path)) {
...
+ private boolean isGeoJSONObjectPath(String path) {
+ return isValidGeometryPath(path) || path.equals("features.[]");
+ }
+
+ private boolean isCrsPropertiesPath(String path) {
+ if (path.equals("crs.properties")) {
+ return true;
+ }
+ String suffix = ".crs.properties";
+ return path.endsWith(suffix)
+ && isGeoJSONObjectPath(path.substring(0, path.length() - suffix.length()));
+ }Trade-offs
- Removes the rejections where
"type"is data, and validates acrson a nested geometry or feature like a top-level one: CRS84 is accepted (main rejects it with the misleading "but got name"), another or a linkedcrsis rejected with the existing messages. - Adds one rejection: a nested
crsthat lacks its required"type"member and names a non-CRS84 or linked system, or whosenameis not a string, which main accepts silently, is rejected, as it already is at the top level. - About 15 lines; no API or format change. Not benchmarked: it adds a few string comparisons per parsed member.
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 B, assertions on and off;
TestPolygon16 of 16; theorg.apache.lucene.geopackage incl. the repro: 18 classes, 194 tests, 0 failures (7 skipped inTestTessellator, the other in-tree caller; the run kept no skip list, and by reading that class has six@Nightlytests and one@AwaitsFixtest, which a default run skips);:lucene:core:check -x testwith the repro in place: BUILD SUCCESSFUL;- a 17-input probe: the repro and 6 wider triggers (non-string, nested, a one-Feature FeatureCollection, a decoy Polygon, a foreign member, a crs-shaped object in
properties) now return the Feature's own polygon, never the decoy; Point, GeometryCollection, and an EPSG:4269 or linkedcrsat the top level or on the geometry are still rejected; a CRS84crson the geometry is accepted.
Only the line-186 change and isGeoJSONObjectPath.
- if (key.equals("type") && path.startsWith("crs") == false) {
+ if (key.equals("type") && isGeoJSONObjectPath(path)) {
...
+ private boolean isGeoJSONObjectPath(String path) {
+ return isValidGeometryPath(path) || path.equals("features.[]");
+ }Trade-offs
The smallest diff (a 1-line condition and a 3-line helper) and fixes every executed trigger. But a crs on a nested geometry is then never checked, because the crs checks run only at the top level: "crs" naming EPSG:4269 or a linked crs on the Feature's geometry, rejected today (with a misleading message), would be accepted and its coordinates read as WGS 84.
What the checks validated
Same scratch worktree: the repro passes (assertions on) and TestPolygon 16 of 16. Probe: the same triggers parse as with B, but a nested EPSG:4269 or linked crs now returns a polygon.
Keep A and throw "crs is only supported at the top level" for a crs member below the top level.
Trade-offs
Also closes A's gap and is simple, but rejects a nested CRS84 crs, which the 2008 spec allows (though it says a crs "should not be repeated or overridden on children or grandchildren"). Not tried.
No search server is known to pass request or document GeoJSON through this method (a code search found only a benchmark call in Elasticsearch and none in OpenSearch; Solr was not checked), so the exposure is applications that call Polygon.fromGeoJSON directly.
Non-formal: a differential parsing probe fed the parser accepted GeoJSON shapes with extra members and compared the result with the coordinates as written; a Feature with a "type" key in properties was rejected. Reading the code found the unscoped "type" check, and a public-API JUnit test reproduced it on the stock build.
The method in full108 words
Method: non-formal (differential testing, then code reading and executed tests). On 2026-10-26 the hunt unit for the LatLon geo queries ran a probe over GeoJSON inputs the parser should accept: Polygon, MultiPolygon, Feature and FeatureCollection with holes, extra members, nulls, exponent numbers and whitespace, each compared with its coordinates as written. While probing extra members it saw a valid Feature rejected because its properties held a "type" key. Reading SimpleGeoJSONPolygonParser found the path.startsWith("crs") == false condition next to the path-scoped "coordinates" check. A public-API JUnit test reproduced it on the stock build; independent checkers reproduced it with their own tests, and it was cut to the one-test repro.
No prior report found in GitHub issues, PRs and commits (incl. the JIRA archive), all 410 open PRs, the Lucene mailing lists, downstream trackers or the web (2026-10-26, again 2026-11-01). LUCENE-8964 (#10007) is a precedent on the same parser; PR #12295 (open as of 2026-11-01) rewrites the same lines without fixing them.
Searches4 searches
Closest items, none a report of this bug: #10007 (LUCENE-8964, the same parser rejected string arrays in properties; fixed in 8.3.0 in parseArray, not in this check); #8433 (LUCENE-7380, the introducing issue; nothing about "type" keys in properties); #12295 (open since 2023-05 and still open in the 2026-11-01 scan, "Use instanceof pattern-matching where possible": rewrites the same lines and keeps the condition, so a fix conflicts with it textually). Outside Go, Kotlin and Rust ports copy the same check; their trackers have nothing on it. The exact error text gives no report anywhere. Related findings in the same parser with other fix sites: LFV-953 and LFV-569. Limits: GitHub search matches tokens, not phrases; one Stack Overflow page could be read only at title level; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene and apache/lucene-solr issues, PRs and commits (incl. the migrated JIRA issues) | 2026-10-26 and 2026-11-01 | two scripted prior-report searches (51 queries, 2338 distinct hits; then 71 queries, 2456 distinct hits), GraphQL and CLI searches for geojson, fromGeoJSON, SimpleGeoJSONPolygonParser, the exact error texts, foreign member, FeatureCollection, geojson properties | no report; closest #10007, #8433, #12295 |
| Open PRs | 2026-11-01 | changed files of all 410 open apache/lucene PRs | only #12295 touches the parser, and keeps the condition |
| JIRA archive, mailing lists, downstream projects, ports, web | 2026-10-26 and 2026-11-01 | JQL for GeoJSON (13 issues read) and the exact phrase (0); dev@, java-user@, issues@ and commits@; Elasticsearch, OpenSearch and Solr trackers; GitHub-wide issue and code search; Go, Kotlin and Rust ports; 8 web queries | no report |
| Upstream main | 2026-11-01 | the parser at 2f424fd428da (2026-10-30) and tag releases/lucene/10.6.0, and its commit list (read-only GitHub API) | line 186 unchanged; newest commit on the file #15681 (2026-02-11, cosmetic) |
Born with the API: LUCENE-7380 (main-line 343f374b530, branch_6x 573aaf75f52, 2016-07-15) added fromGeoJSON and this check; first release 6.2.0 (the backport). All 90 releases from 6.2.0 to 10.6.0 and main fail the repro (executed); 6.1.0 has no fromGeoJSON. Not fixed upstream.
Versions tested5 rows
Introduced by LUCENE-7380 "Add Polygon.fromGeoJSON" (#8433; JIRA patch era, no GitHub PR; committed by Mike McCandless after a +1 from Robert Muir): main-line commit 343f374b530 and branch_6x backport 573aaf75f52, both 2016-07-15; 6.2.0 shipped the backport, and the main-line commit first shipped in 7.0.0. The commit created the parser with this condition, so it is not a regression; git log -S on the condition from HEAD returns only that commit. Later commits (spelling fixes, LUCENE-8964 in parseArray, the MutiPolygon message typo fix first released in 8.4.0, a reformat, lint changes, isEmpty(), and #15681 in 2026) do not change which paths the check covers, though LUCENE-8964 did change what parseArray accepts. 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) |
| 10.5.2 | affected | minimal repro fails with assertions on and off; the verifiers' wider tests fail too |
| 6.2.0 to 10.5.1 (88 releases) | first affected 6.2.0 | release sweep: the minimal repro (or a portable copy with the old test-framework import and its own class name) fails on every one; 6.2.0 also with assertions off; a control without the key passes |
| 6.1.0 | no fromGeoJSON | the test does not compile: the API does not exist yet |
Confirmed, high confidence: the finder, an independent reproducer, an intended-behaviour devil's advocate, the adjudication, a credibility audit, a re-verification and a tie-breaker all reproduced it. A read-only reviewer from another model family said "not real" only because it could not run code, and agreed with the root cause.
Checks and verdicts336 words
Everyone agrees on the root cause and that the input is in contract. The tie-breaker found that the study's fix sketch would stop checking a crs on a nested geometry; the recommended fix covers that.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | 3-test JUnit (one control) fails 2 of 3 on main with assertions on and off, 10.5.2, 9.12.3, 9.0.0, 8.11.2 and 7.7.3; with a path-scoped fix 3 of 3 and upstream TestPolygon 16 of 16. |
| cross-model review, intended lens (OpenAI GPT-5.6, read-only) | real, medium | The reviewer (OpenAI GPT-5.6) ran no code; by reading it rated the bug real, medium, and the intended-behaviour argument weak. A separate re-run of the finder's 3-test class: 2 of 3 fail on main ea57360912ed. A separate 4-case probe: 4 of 4 fail on main; with the fix sketch only the foreign member literally named "geometry" still fails. |
| independent reproducer | real | Its own JUnit outside the geo package (2 controls, 6 cases) fails 6 of 8 on main (assertions on and off), 10.5.2, 9.12.3 and 8.11.4; 8 of 8 with the fix. |
| intended-behaviour devil's advocate | real, high; severity low | 8-case JUnit (geocoder-shaped FeatureCollection, map tags, the RFC 7946 centerline member, two intended rejections) fails 4 of 8 on main, 10.5.2 and 9.12.3; 8 of 8 with the fix. |
| adjudication | confirmed, high | Re-ran the reproducer's JUnit (6 of 8 fail with assertions on and off) and the finder's. |
| credibility audit | confirmed | Own public-API program: 2 controls accept, 4 of 4 in-contract inputs rejected. |
| cross-model review, refute lens (OpenAI GPT-5.6, read-only) | not real, low (procedural) | Its session could not run code; it agreed with the root cause and that the inputs are in contract, and rated the intended argument weak. |
| re-verification and tie-breaker | real, high | Minimal test fails on main (10 of 10 seeds), 10.6.0 and 10.5.2; 8 wider cases fail on main and 10.6.0 and pass with the fix; TestPolygon 16 of 16 with and without it. |
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 413 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,442 words
- Registry title
- Polygon.fromGeoJSON rejects a valid GeoJSON Feature whose properties (or another non-geometry member object) contain a member named "type", throwing ParseException 'can only handle type ... but got <value>' (every release since 6.2.0)
- Area
- core / geo GeoJSON parsing (Polygon.fromGeoJSON, SimpleGeoJSONPolygonParser)
- Priority
- Low, rank 1071: Polygon.fromGeoJSON loudly rejects valid Features whose properties contain a 'type' key, reachable by real data but in an experimental method with no production callers.
- 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, and a read-only code search found only a benchmark call in Elasticsearch and none in OpenSearch) with whole GeoJSON Features rather than bare geometries: a Feature whose properties, or an object nested in them, hold a key "type" (for example a geocoder's 'type' field or a map tag type=multipolygon) is rejected with ParseException, and so is a Polygon carrying a foreign member object with a "type" key; no setting is involved, no wrong polygon is ever returned, nothing hangs and no index is touched, no scale is needed, with -ea and -da, in every release from 6.2.0 to 10.6.0 and main.
- Summary
- You call Polygon.fromGeoJSON on a standard GeoJSON Feature whose geometry is a single Polygon and whose properties contain an ordinary data member named "type", for example {"type": "Feature", "geometry": {...Polygon...}, "properties": {"type": "multipolygon"}}, and expect the polygon back, because the javadoc accepts a Polygon 'optionally embedded under a "type: Feature"' and GeoJSON allows any JSON object as properties. Instead it throws ParseException 'can only handle type FeatureCollection (if it has a single polygon geometry), Feature, Polygon or MultiPolygon, but got multipolygon' ('type should be a string, but got: 3.0' for {"type": 3}), because the parser reads every "type" member outside crs as a GeoJSON object type, wherever it sits, while it reads "coordinates" only at geometry positions. The same Feature without the "type" property parses, and a "coordinates" key inside properties is ignored.
- Introduced
- 343f374b530fa71dc6102d74725b536f5f1367f3 (main line, 2016-07-15, 'LUCENE-7380: add Polygon.fromGeoJSON', committed by @mikemccand after a +1 from @rmuir; 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 check was born with the file and is not a regression [read: git log -S on 'key.equals("type") && path.startsWith("crs") == false' and git log -G on 'boolean isValidGeometryPath' return only 343f374b530; git blame -w --ignore-rev 2d6ad2fee6d of lines 186-205 gives 16 lines to it, 3 to the message-typo fix f70e21c and 1 to the isEmpty change cbec94c; no later commit (including LUCENE-8964, 5f6c744ae1b) changes which paths the check covers or which values it accepts; exec: 6.1.0 has no fromGeoJSON, so the repro does not compile there, and 6.2.0 fails] (2016-07-15)
- First affected version
- 6.2.0 [exec] (the first release with Polygon.fromGeoJSON and SimpleGeoJSONPolygonParser; 6.1.0 has neither); first 7.x release 7.0.0 [exec], from the main-line commit
- Affected versions
- every release from 6.2.0 through 10.6.0, and main ea57360912ed. [exec 2026-11-01] a sweep of all 90 releases from 6.2.0 to 10.6.0 fails the one-test minimal repro (up to 9.0.0 a copy with the old test-framework import) with -ea, and 6.2.0, 10.6.0 and main 645d4fe14a20 fail it with -da too; earlier the finder's JUnit (one control, two properties cases) fails 2 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 (-ea and -da), 10.0.0, 10.5.2 (-ea and -da) and main (-ea and -da) in the version-history runs, and on 7.7.3, 8.11.2, 9.0.0, 9.12.3 and 10.5.2 in the finder's runs (6.2.0, 6.6.6 and 7.0.0 with the junitcore runner, because their test framework pins junit 4.10; releases up to 8.0.0 print the original 'MutiPolygon' message, so each run used its own release's parser); the independent reproduction's 8-case JUnit fails 6 of 8 on main (-ea and -da), 10.5.2, 9.12.3 and 8.11.4; the intended-behaviour check's fails 4 of 8 on main (-ea and -da), 10.5.2 and 9.12.3. [read] the line is in all 80 non-RC release tags of the history clone that have the file (6.2.0-6.6.6, 7.0.0-7.7.3, 8.0.0-8.8.1, 9.0.0-9.12.3, 10.0.0-10.5.2); 8.8.2-8.11.3 are not tagged in the clone (all of them executed in the sweep). The verifiers' wider variants (foreign member, non-string value, FeatureCollection) ran on main, 10.6.0 (the tie-breaker's 8 wider cases), 10.5.2, 9.12.3 and 8.11.4 only and are inferred for older releases (same line). Not fixed upstream: upstream main a95c754ee003 (2026-10-26) has no newer commit on the parser. aarch64 with JDK 25 only (pure string parsing)
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an adjudication that 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
- Upstream and prior reports
- none found (2026-10-26, read-only): a scripted prior-report search (51 queries: 16 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list over dev@, java-user@, issues@ and commits@, 4 JIRA, 6 web; 2338 distinct hits, 0 high, 16 medium, all triaged) plus the finder's run (10 queries), manual GitHub GraphQL and REST searches on apache/lucene, apache/lucene-solr, elastic/elasticsearch, opensearch-project/OpenSearch, apache/solr and all of GitHub (the exact error text gives 0 hits everywhere), the trackers of 10 downstream callers or ports, the changed files of all 406 open apache/lucene PRs, JIRA text searches with the 13 LUCENE GeoJSON issues read, mailing-list searches, 7 web queries and the code history. Nothing reports or discusses a "type" member outside the geometry being rejected. Closest items, not reports of this bug: LUCENE-8964 (#10007, fix PR apache/lucene-solr#866, 8.3.0): the same parser rejected other legal properties content (string arrays), fixed in parseArray without touching the type check, and this bug still reproduces after it; LUCENE-7380 (#8433): the introducing issue, whose 7 comments never mention "type" keys in properties; #12295 (open since 2023-05): an instanceof refactor of the same lines that keeps the path condition (not a fix; would conflict textually with one). Not fixed upstream: upstream main a95c754ee003 has no newer commit on the parser (last 448c7d8e241, #15681, cosmetic). Search limits: one Stack Overflow page could not be fetched (its snippet does not mention the error); GitHub issue search is token-based, so "type" was always paired with GeoJSON terms or the exact error text; private channels were not searched
- Lucene commit tested
- main
645d4fe14a20(the minimal repro with assertions on and off, standalone and through gradle; two fix options in a scratch worktree, withTestPolygon, theorg.apache.lucene.geotest package and:lucene:core:check); the 10.6.0 and 10.5.2 release jars (checksums verified against Maven Central; assertions on and off); a sweep of all 90 releases from 6.2.0 to 10.6.0; 6.1.0 has nofromGeoJSON. Earlier, in the study: mainea57360912edand releases 10.5.2, 9.12.3, 9.0.0, 8.11.4, 8.11.2 and 7.7.3 with the verifiers' own tests. Upstream main2f424fd428da(2026-10-30) has the same parser (read).
- Finding record
lucene/core/src/java/org/apache/lucene/geo/SimpleGeoJSONPolygonParser.java:23-45 (at ea57360912ed) (header comment: a Feature with properties holding a nested object is accepted), :47-51 ('Does minimal parsing'), :186-204 ('type' check: 'key.equals("type") && path.startsWith("crs") == false'; non-string throw at :187-190; Polygon/MultiPolygon only on isValidGeometryPath; Feature/FeatureCollection only at '' or features.[]; else throw 'can only handle type ...'), :205-215 ('coordinates' read only when isValidGeometryPath), :222-224 (isValidGeometryPath)lucene/core/src/java/org/apache/lucene/geo/Polygon.java:41 (at ea57360912ed) (@lucene.experimental), :248-257 (fromGeoJSON javadoc 'Parses a standard GeoJSON polygon string ... optionally embedded under a "type: Feature"', link to the 2008 GeoJSON spec, throws ParseException)SECURITY.md lines 28, 38, 42, 50 (threat model)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/geo/SimpleGeoJSONPolygonParser.java:23-51, :65, :146-153, :186-205, :221-224, :310-313 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/geo/Polygon.java: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 (only Polygon.java and tests)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/geo/Polygon.java:41, :248-257 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/geo/SimpleGeoJSONPolygonParser.java:23-51, :186, :205 (at 645d4fe14a20)lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java:176-199, :268-281, :283-303, :305-346 (at 645d4fe14a20)lucene/CHANGES.txt:8978-8980, :6239-6240 (at 645d4fe14a20)
- Suggested fix
https://www.rfc-editor.org/rfc/rfc7946.txt (section 7.1; fetched 2026-11-01)lucene/core/src/test/org/apache/lucene/geo/TestTessellator.java:38, :80, :717, :733, :789, :903, :909 (at 645d4fe14a20; @AwaitsFix, @Nightly)
- TestGeoJSONFeatureTypeProperty.java (JUnit, one test):
repro/LFV-562/src/org/apache/lucene/geo/TestGeoJSONFeatureTypeProperty.java(35 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the repro's input and output; step 5 and the summary card were redone after the fact checks to show the recommended fix B (re-run on main, fix A and fix B, -ea and -da) and the 90-release sweep.
assets/LFV-562/bug.png(413 KB)
