All findings · Lucene finding · LFV-562 · geojson-type-member-in-properties-rejected

Polygon.fromGeoJSON rejects a GeoJSON Feature whose properties contain a "type" member

Polygon.fromGeoJSON accepts a Polygon inside a GeoJSON Feature, but throws ParseException: ... but got x when the Feature's free-form properties hold a key named "type", such as {"type": "x"}. The parser reads every "type" key as a GeoJSON object type; affected since 6.2.0.

Summary

Priority
Low, rank 1071Polygon.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
wrong error
Since
6.2.0 (LUCENE-7380; branch_6x commit 573aaf75f52, 2016-07-15)introduced 2016-07-15 · over 10 years
Affected
all 90 releases 6.2.0-10.6.0, and main
Status
documented
Found by
non-formal: a differential parsing probe, then a public-API test

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

Background: GeoJSON Features in Lucene

  1. 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; properties is any JSON object the producer chooses.
  2. Polygon.fromGeoJSON(String) in Lucene core turns a Polygon or MultiPolygon, bare, inside a Feature, or inside a one-Feature FeatureCollection, into Polygon[], and throws ParseException for anything else (a Point, several features, a non-CRS84 crs).
Diagram of the bug: Polygon.fromGeoJSON parses a GeoJSON Feature. At the geometry, "type": "Polygon" and "coordinates" are read and give the polygon. Inside properties, the key "coordinates" is skipped, but the key "type" with value "x" is also checked as a GeoJSON object type; x is not Feature, FeatureCollection, Polygon or MultiPolygon, so the parser throws ParseException 'can only handle type ... but got x' and no polygon is returned. The recommended fix (B) reads "type" only where a GeoJSON object can be and checks the crs of each such object; the type-only sketch (A) would stop rejecting a nested EPSG:4269 crs.
The parser checks every "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.

The finding, step by step

  1. 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.
  2. Change the property to "properties": {"type": "x"}. GeoJSON lets properties be any JSON object, and the javadoc accepts a Polygon "optionally embedded under a "type: Feature"", so you expect the same 1 polygon.
  3. 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 outside crs as a GeoJSON object type (line 186), so x looks like an unsupported geometry.
  4. Its sibling key "coordinates" is read only at geometry positions, so {"coordinates": [...]} in properties is 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.

Who is affected

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.

Terms

  • SimpleGeoJSONPolygonParser: the package-private hand-written parser behind Polygon.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.

The bug

  1. parseObject parses a member whose value is an object recursively, with path path + "." + key (:146-153); so the Feature's properties object is read with path properties.
  2. After each member, line 186 asks if (key.equals("type") && path.startsWith("crs") == false): every "type" member outside crs is read as a GeoJSON object type, wherever it sits.
  3. Inside that branch, a non-string value throws type should be a string (:187-190); Polygon and MultiPolygon are accepted only on a geometry path (:192-195); Feature and FeatureCollection only at "" or features.[] (:196-197); anything else throws can only handle type ... but got <value> (:199-204). At path properties no value can pass.
  4. The next check, key.equals("coordinates") && isValidGeometryPath(path) (:205), is scoped by path. That asymmetry is the bug: "coordinates" in properties is ignored, "type" in properties is fatal.

The numbers

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 145

Also 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; Polygon is @lucene.experimental, Polygon.java:41) that pass Features or one-Feature FeatureCollections whose properties (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 the properties fields "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 with polygon_geojson, and the parser accepts a FeatureCollection only with a single polygon. In OpenStreetMap a multipolygon relation carries the tag type=multipolygon; data that copies such tags into properties is rejected the same way (... but got multipolygon).
  • Reach. In the Lucene tree the method has no production caller, only TestPolygon and TestTessellator. 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 geometry object, or drop properties before calling.
  • What does not happen. No wrong or different polygon (a decoy geometry in properties is rejected, never used), no hang, no extra cost beyond a normal parse, nothing written to an index.

Minimal repro

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.

TestGeoJSONFeatureTypeProperty.java (JUnit, one test)35 lines
/*
 * 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);
    }
  }
}
Command
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 127 from SimpleGeoJSONPolygonParser.java:201; the same with assertions off.

All files of this repro: repro/LFV-562/

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.

Environment

lucenemain 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
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64
Command (environment)
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)

Observed output

# 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

Why it looks unintended

  • The javadoc accepts a Polygon "optionally embedded under a "type: Feature"" and links the GeoJSON spec, where properties is "any JSON object".
  • The parser already reads "coordinates" only at geometry positions and exempts crs from the "type" check; properties was not exempted.
  • No test asserts this rejection; LUCENE-8964 already fixed this parser for rejecting other legal properties content.
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), and crs is exempt from the "type" check (:186) because a crs object has its own "type" (testIllegalGeoJSONLinkedCRS even has "type": "proj4" under crs.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" at features.[].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 legal properties content and the project fixed it, in parseArray; 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.

Suggested fix

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 a crs on 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 linked crs is rejected with the existing messages.
  • Adds one rejection: a nested crs that lacks its required "type" member and names a non-CRS84 or linked system, or whose name is 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;
  • TestPolygon 16 of 16; the org.apache.lucene.geo package incl. the repro: 18 classes, 194 tests, 0 failures (7 skipped in TestTessellator, the other in-tree caller; the run kept no skip list, and by reading that class has six @Nightly tests and one @AwaitsFix test, which a default run skips);
  • :lucene:core:check -x test with 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 linked crs at the top level or on the geometry are still rejected; a CRS84 crs on the geometry is accepted.

A. Scope "type" only (the study's fix sketch) (validated by the checks)

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.

A plus an explicit rejection of nested crs (not tried)

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.

Downstream note

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.

How it was found

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.

Prior reports, history and checks

Prior reports

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.

What was searched, when, and what turned up
WhereWhenHowFound
apache/lucene and apache/lucene-solr issues, PRs and commits (incl. the migrated JIRA issues)2026-10-26 and 2026-11-01two 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 propertiesno report; closest #10007, #8433, #12295
Open PRs2026-11-01changed files of all 410 open apache/lucene PRsonly #12295 touches the parser, and keeps the condition
JIRA archive, mailing lists, downstream projects, ports, web2026-10-26 and 2026-11-01JQL 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 queriesno report
Upstream main2026-11-01the 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)

History

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).

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal repro fails with assertions on and off (standalone and through Gradle); passes with fix A or B
10.6.0 affectedminimal repro fails with assertions on and off (release jars, checksums verified)
10.5.2 affectedminimal 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.0release 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 fromGeoJSONthe test does not compile: the API does not exist yet

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidate3-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, mediumThe 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 realIts 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 low8-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, highRe-ran the reproducer's JUnit (6 of 8 fail with assertions on and off) and the finder's.
credibility audit confirmedOwn 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, highMinimal 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.

Diagram file

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.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,442 words

Finding record

LFV-562, snapshot of 2026-11-06

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, with TestPolygon, the org.apache.lucene.geo test 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 no fromGeoJSON. Earlier, in the study: main ea57360912ed and 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 main 2f424fd428da (2026-10-30) has the same parser (read).

Lucene and upstream sources

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)

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

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.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.