Repository navigation
OPENNLP-1931: Regex removal (3a/10): Read opennlp-dl vocab and config JSON with a strict scanner - #1277
Conversation
9ba4095 to
944ab39
Compare
rzo1
left a comment
There was a problem hiding this comment.
Little time, so here is a GPT 5.6-sol review instead for now
No blocking findings. No additional API or parsing regression found. The old and new JSON implementations agreed across 60,000 generated cases.
Validation across the combined stack: 1,856 targeted tests, zero failures, one skipped.
944ab39 to
fed91ba
Compare
|
Here are some additional comments. Def. needs an eval build ( Blocking
Minor
Verified:
|
1 similar comment
|
Here are some additional comments. Def. needs an eval build ( Blocking
Minor
Verified:
|
|
Follow-up: the pre-existing BOM problem in |
8699305 to
a4ed75a
Compare
|
CI is red. |
f632f55 to
ed9dd7e
Compare
|
Thanks for the update. CI is green now, and the description is refreshed. I have three points on the code. 1. Please move the Doing it properly needs more than this PR should carry. For this PR, please:
Nothing is released between the two PRs, since 2. Mark
3. Reference OPENNLP-1953 The byte order mark fix in The eval build ( |
3c7012f to
399e50d
Compare
rzo1
left a comment
There was a problem hiding this comment.
Blocking
-
AbstractDL.java
loadJsonVocab: behavior change that isn't documented. The old regex picked up every"token": idpair anywhere in the file, so atokenizer.jsonpassed as the vocabulary happened to load. It is now rejected, and so is anyvocab.jsonwith non-integer members. That's the right call, but users will only see it as a newInvalidFormatException. Please say indoccat.xmlthattokenizer.jsonisn't accepted as a vocabulary yet, with a pointer to OPENNLP-1988. -
Eval build. This changes how DL vocabularies and labels are loaded. Please trigger an eval build on the final head before merge. The local
DocumentCategorizerDLEvalrun is good, but not enough.
Minor
- AbstractDL.java:56, :196 and JsonScan.java:59, :286. Byte-order-mark handling is duplicated:
AbstractDLstrips the BOM, and thenJsonScanchecks for it again.FrequencyDictionaryLoader(#1320) andHunspellDictionarydo the same. Please add oneStringUtil.stripByteOrderMarkand use it everywhere. - JsonScan.java:98, :124, :200.
"key must not be null"is repeated three times, whileTEXT_MUST_NOT_BE_NULLis already a constant. DeclareKEY_MUST_NOT_BE_NULL. - JsonScan.java:350-351.
indexOf(BACKSLASH, start, end)is already bounded, sofirstBackslash >= endcan never be true. Drop it. - JsonScan.java:48-49.
@Internaldoesn't change that this ships as a public type. It's public only becauseDocumentCategorizerConfigsits inopennlp.dl.doccat. Move theid2labellookup intoopennlp.dland makeJsonScanpackage-private, or say why it has to be public. - JsonScan.java:34-46. The class Javadoc explains the design ("in one pass", "Nesting is bounded by memory, not by the call stack", "the only API of this class"). Cut it down to what the class does, RFC 8259 plus the documented deviations, and move the rationale to the JIRA ticket.
- doccat.xml:176-199. Both paragraphs are too detailed ("Thus a BOM-prefixed…", "U+FEFF inside a JSON string is content…", "so a damaged vocabulary is reported instead of read in part", Python's
NaN). Condense each to two or three sentences. :185 is also much longer than its neighbours; please rewrap it. - LoadVocabTest.java. The
File.createTempFile+Files.writeStringsetup is repeated in several tests, andDocumentCategorizerDLTest:211already has aconfigFile(...)helper. Use@TempDirwith one shared helper. - Description. "Configuration whitespace handling is independent of
opennlp.whitespace.mode" only makes sense next to the other regex-removal PRs. Drop it.
OPENNLP-1931: Strip byte order marks through StringUtil (for apache apache#1277)
…inors OPENNLP-1931: Address rzo1's remaining review asks (for apache apache#1277) Human sign-off
399e50d to
16148bb
Compare
|
Once green, going to run the eval build |
|
@krickert Please link the eval build here. |
|
Thanks for the update. Maintainer edits don't work on the
Apply it with 1277.patchFrom fb41615b1b87be53ed9398367978f17be16f26c0 Mon Sep 17 00:00:00 2001
From: Richard Zowalla <rzo1@apache.org>
Date: Fri, 25 Sep 2026 21:10:17 +0200
Subject: [PATCH] OPENNLP-1931: Trim commentary and name the bad id2label key
- JsonScan: reduce the class Javadoc to what is read and the three
deviations from RFC 8259.
- DocumentCategorizerDL: report a non-integer id2label key by name
instead of the bare NumberFormatException message.
- StringUtilByteOrderMarkTest: use escapes instead of invisible
characters.
- doccat.xml: condense the vocabulary and configuration paragraphs and
link OPENNLP-1988.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
.../util/StringUtilByteOrderMarkTest.java | 4 +--
.../src/main/java/opennlp/dl/JsonScan.java | 26 +++++++++----------
.../dl/doccat/DocumentCategorizerDL.java | 17 +++++++++++-
opennlp-docs/src/docbkx/doccat.xml | 23 ++++++----------
4 files changed, 38 insertions(+), 32 deletions(-)
diff --git a/opennlp-api/src/test/java/opennlp/tools/util/StringUtilByteOrderMarkTest.java b/opennlp-api/src/test/java/opennlp/tools/util/StringUtilByteOrderMarkTest.java
index 0914c7673..8f34a0a90 100644
--- a/opennlp-api/src/test/java/opennlp/tools/util/StringUtilByteOrderMarkTest.java
+++ b/opennlp-api/src/test/java/opennlp/tools/util/StringUtilByteOrderMarkTest.java
@@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test;
*/
public class StringUtilByteOrderMarkTest {
- private static final String BOM = "";
+ private static final String BOM = "\uFEFF";
@Test
void testStripsOneLeadingMark() {
@@ -49,7 +49,7 @@ public class StringUtilByteOrderMarkTest {
Assertions.assertFalse(StringUtil.startsWithByteOrderMark(""));
Assertions.assertFalse(StringUtil.startsWithByteOrderMark("{}" + BOM));
// U+FFFE is the byte-swapped mark, not a mark itself
- Assertions.assertFalse(StringUtil.startsWithByteOrderMark("�{}"));
+ Assertions.assertFalse(StringUtil.startsWithByteOrderMark("\uFFFE{}"));
}
@Test
diff --git a/opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/JsonScan.java b/opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/JsonScan.java
index a5363c93f..4a0246a86 100644
--- a/opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/JsonScan.java
+++ b/opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/JsonScan.java
@@ -30,20 +30,18 @@ import opennlp.tools.commons.Internal;
import opennlp.tools.util.StringUtil;
/**
- * Reads the JSON files of the deep-learning components, vocabularies and model configurations,
- * in one pass over the text. Structure, whitespace, numbers, and string escapes follow
- * <a href="https://www.rfc-editor.org/rfc/rfc8259">RFC 8259</a>, with three additions: a byte
- * order mark as the first character is skipped, as
- * <a href="https://www.rfc-editor.org/rfc/rfc8259#section-8.1">section 8.1</a> allows; the
- * control characters {@code U+0000} to {@code U+001F} are accepted as content inside a string,
- * in keys as well as in values; and the values {@code NaN}, {@code Infinity}, and
- * {@code -Infinity}, which Python's {@code json} module writes by default, are accepted where a
- * value is skipped, never where one is read. Nesting is bounded by memory, not by the call
- * stack. Malformed text is reported as an {@link IllegalArgumentException} whose message names
- * the offset at which reading stopped.
- *
- * <p>{@link #stringObject(String, String)} is the only API of this class; its other members
- * serve the classes of this package.
+ * Reads the JSON files of the deep-learning components, vocabularies and model configurations.
+ * Structure, whitespace, numbers, and string escapes follow
+ * <a href="https://www.rfc-editor.org/rfc/rfc8259">RFC 8259</a>, with three additions:
+ * <ul>
+ * <li>a byte order mark as the first character is skipped, as
+ * <a href="https://www.rfc-editor.org/rfc/rfc8259#section-8.1">section 8.1</a> allows;</li>
+ * <li>the control characters {@code U+0000} to {@code U+001F} are accepted inside a string;</li>
+ * <li>the values {@code NaN}, {@code Infinity}, and {@code -Infinity} are accepted where a
+ * value is skipped, never where one is read.</li>
+ * </ul>
+ * Malformed text is reported as an {@link IllegalArgumentException} whose message names the
+ * offset at which reading stopped.
*/
@Internal(since = "3.0.0")
public final class JsonScan {
diff --git a/opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/doccat/DocumentCategorizerDL.java b/opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/doccat/DocumentCategorizerDL.java
index ca9fd0667..208cb573e 100644
--- a/opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/doccat/DocumentCategorizerDL.java
+++ b/opennlp-core/opennlp-ml/opennlp-dl/src/main/java/opennlp/dl/doccat/DocumentCategorizerDL.java
@@ -453,7 +453,7 @@ public class DocumentCategorizerDL extends AbstractDL implements DocumentCategor
final Map<Integer, String> categories = new HashMap<>();
try {
for (Map.Entry<String, String> label : DocumentCategorizerConfig.fromJson(json).id2label().entrySet()) {
- categories.put(Integer.valueOf(label.getKey()), label.getValue());
+ categories.put(parseIndex(label.getKey()), label.getValue());
}
} catch (IllegalArgumentException e) {
throw new InvalidFormatException(
@@ -462,4 +462,19 @@ public class DocumentCategorizerDL extends AbstractDL implements DocumentCategor
return categories;
}
+ /**
+ * Parses an {@code id2label} key as an output index.
+ *
+ * @param key The key to parse.
+ * @return The output index.
+ * @throws IllegalArgumentException Thrown if {@code key} is not an integer.
+ */
+ private static int parseIndex(String key) {
+ try {
+ return Integer.parseInt(key);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("id2label key must be an integer: " + key, e);
+ }
+ }
+
}
diff --git a/opennlp-docs/src/docbkx/doccat.xml b/opennlp-docs/src/docbkx/doccat.xml
index 7b74274c1..b41729334 100644
--- a/opennlp-docs/src/docbkx/doccat.xml
+++ b/opennlp-docs/src/docbkx/doccat.xml
@@ -173,24 +173,17 @@ String category = myCategorizer.getBestCategory(outcomes);]]>
For additional examples, refer to the <code>DocumentCategorizerDLEval</code> class.
</para>
<para>
- The vocabulary file is either plain text, with one token per line and the line number as
- the token ID, as in <code>vocab.txt</code>, or a JSON object that maps each token to a
- non-negative integer ID, as in <code>vocab.json</code>. A file is read as JSON when its
- first non-whitespace character is an opening brace, and a leading byte order mark is
- ignored. A Hugging Face <code>tokenizer.json</code> is not a vocabulary file and is
- rejected; support for it is tracked in OPENNLP-1988. A vocabulary that cannot be read is
- rejected with an <code>InvalidFormatException</code> that names the file and the position
- of the problem.
+ The vocabulary file is either plain text with one token per line, as in
+ <code>vocab.txt</code>, or a JSON object mapping each token to an integer ID, as in
+ <code>vocab.json</code>. A Hugging Face <code>tokenizer.json</code> is not supported yet
+ (<ulink url="https://issues.apache.org/jira/browse/OPENNLP-1988">OPENNLP-1988</ulink>).
+ An unreadable vocabulary is rejected with an <code>InvalidFormatException</code>.
</para>
<para>
When a configuration file is given in place of the categories map, its top-level
- <code>id2label</code> object supplies the categories, mapping each output index to a
- label. A configuration without a top-level <code>id2label</code> gives no categories.
- A configuration that is not well-formed JSON, or whose <code>id2label</code> is not an
- object of strings keyed by integers, is rejected with an
- <code>InvalidFormatException</code>. The values <code>NaN</code> and
- <code>Infinity</code>, which Python writes into some configurations, are accepted
- outside <code>id2label</code>.
+ <code>id2label</code> object maps each output index to a category label. A configuration
+ that is not well-formed JSON, or whose <code>id2label</code> is not an object of strings
+ keyed by integers, is rejected with an <code>InvalidFormatException</code>.
</para>
<para>
Like <code>NameFinderDL</code>, long input is split into overlapping chunks on the full
--
2.55.0
Still open on your side:
As mentioned on #1278: pushing your branches to |
|
Thanks. I'll push all of them to Apache branches tonight |
rzo1's 2026-09-25 patch on apache apache#1277: JsonScan class Javadoc reduced to what it reads and the deviations from RFC 8259, a non-integer id2label key reported by name, escapes instead of invisible characters in the byte order mark test, and condensed doccat.xml paragraphs with OPENNLP-1988 as a link. Squash of #25, patch by Richard Zowalla, reviewed by Kristian Rickert.
Replace regex parsing with JsonScan for JSON vocabularies and categorizer configuration. Require nonnegative integer token ids and read labels from the top-level id2label object, decoding escaped keys and labels. Report malformed files as InvalidFormatException with the file name and offset. Skip nested values without recursive calls and accept non-finite numbers only in skipped values. Handle a leading byte order mark in JSON and plain-text vocabularies and config.json, fixing OPENNLP-1953. Keep tokenizer.json support separate under OPENNLP-1988. Add tests for malformed input, escapes, nesting, token ids and byte order marks. Document the accepted formats and errors in the manual. Assisted-by: Claude Code 2.1.281 Co-authored-by: Kristian Rickert <kristian@apache.org> Co-authored-by: Richard Zowalla <rzo1@apache.org> Co-authored-by: Claude <noreply@anthropic.com> Reviewed-by: Kristian Rickert <kristian@apache.org> Signed-off-by: Kristian Rickert <kristian@apache.org>
16148bb to
a782ae6
Compare
|
There's 2 eval builds running right now. I'll start a new one for this one tomorrow AM EST |
tokenizer.json vocabularies: recognized by model.type with WordPiece only, added_tokens added with conflicts rejected, normalizer.lowercase checked against lowerCase, a flat vocabulary with a bad value names both accepted layouts, hand-written fixture with encodings computed by hand, docs once in doccat.xml with the other DL chapters pointing to it. Rebased onto the apache#1277 head a782ae6. Squash of #27, reviewed by Kristian Rickert.
https://ci-builds.apache.org/job/OpenNLP/job/eval-tests-configurable/87/ (green) |
Replaces regex parsing in
opennlp-dlwithJsonScanfor vocabulary and configuration files. Handles escaped tokens and labels, reports malformed input, and accepts leading byte order marks.AI assisted summary, reviewed and edited: