diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4b8ffb6..9976324 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -106,11 +106,8 @@ This project uses [Gradle Dependency Verification](https://docs.gradle.org/curre
When you **add or update dependencies**, you must regenerate the verification metadata before pushing:
```bash
-# Regenerate verification metadata (includes all configurations)
-./gradlew build --write-verification-metadata sha256
-
-# If you also use CycloneDX SBOM generation, update that too
-./gradlew cyclonedxBom --write-verification-metadata sha256
+# Regenerate verification metadata (includes build and CycloneDX SBOM configurations)
+./gradlew --write-verification-metadata sha256 build cyclonedxBom -x test
```
Commit the updated `gradle/verification-metadata.xml` with your dependency changes. The CI build will **fail** if checksums are missing.
diff --git a/README.md b/README.md
index 93c1d03..d1bda16 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[](https://github.com/toon-format/toon-java/actions/workflows/release.yml)
[](https://central.sonatype.com/artifact/dev.toonformat/jtoon)

-[](https://github.com/toon-format/spec)
+[](https://github.com/toon-format/spec)
[](./LICENSE)
Compact, human-readable serialization format for LLM contexts with **30-60% token reduction** vs JSON. Combines YAML-like indentation with CSV-like tabular arrays. Working towards full compatibility with the [official TOON specification](https://github.com/toon-format/spec).
@@ -351,12 +351,25 @@ Object result2 = JToon.decode(invalidToon, lenient);
**CI/CD:** GitHub Actions • Java 17 • Coverage enforcement • PR coverage comments
+## Development
+
+```bash
+# Build (includes tests, checks, coverage)
+./gradlew build
+
+# Run tests only
+./gradlew test
+
+# Update dependency verification metadata (required when adding/updating dependencies)
+./gradlew --write-verification-metadata sha256 build cyclonedxBom -x test
+```
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) for full development guidelines.
+
## Project Status
This project is 100% compliant with TOON specification. Release conformance enforced on CI/CD.
-See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
-
## Documentation
- [📘 Full Documentation](docs/) - Extended guides and references
diff --git a/build.gradle b/build.gradle
index 9fc8575..bd93fec 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,11 +3,11 @@ plugins {
id 'maven-publish'
id 'signing'
id 'jacoco'
- id 'com.github.spotbugs' version '6.5.8'
+ id 'com.github.spotbugs' version '6.5.9'
id 'pmd'
id 'checkstyle'
id 'org.owasp.dependencycheck' version '12.2.2'
- id 'org.cyclonedx.bom' version '3.2.4'
+ id 'org.cyclonedx.bom' version '3.3.0'
id 'info.solidsoft.pitest' version '1.19.0'
id 'net.ltgt.errorprone' version '5.1.0'
}
@@ -146,7 +146,7 @@ tasks.checkstyleTest {
dependencies {
implementation 'tools.jackson.core:jackson-databind:3.2.1'
implementation 'tools.jackson.module:jackson-module-blackbird:3.2.1'
- compileOnly 'com.github.spotbugs:spotbugs-annotations:4.10.2'
+ compileOnly 'com.github.spotbugs:spotbugs-annotations:4.10.3'
// NullAway + Error Prone for compile-time null safety
errorprone 'com.uber.nullaway:nullaway:0.13.7'
@@ -158,7 +158,7 @@ dependencies {
}
api 'org.jspecify:jspecify:1.0.0'
- testImplementation platform('org.junit:junit-bom:6.1.1')
+ testImplementation platform('org.junit:junit-bom:6.1.2')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'org.awaitility:awaitility:4.3.0'
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 391025f..3191973 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -247,6 +247,11 @@
+
+
+
+
+
@@ -269,6 +274,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -288,6 +304,14 @@
+
+
+
+
+
+
+
+
@@ -1110,6 +1134,11 @@
+
+
+
+
+
@@ -1563,6 +1592,11 @@
+
+
+
+
+
@@ -1571,6 +1605,14 @@
+
+
+
+
+
+
+
+
@@ -1579,6 +1621,14 @@
+
+
+
+
+
+
+
+
@@ -1587,6 +1637,14 @@
+
+
+
+
+
+
+
+
@@ -1595,6 +1653,14 @@
+
+
+
+
+
+
+
+
@@ -1603,6 +1669,14 @@
+
+
+
+
+
+
+
+
@@ -1618,6 +1692,11 @@
+
+
+
+
+
@@ -1626,6 +1705,14 @@
+
+
+
+
+
+
+
+
@@ -1634,6 +1721,14 @@
+
+
+
+
+
+
+
+
@@ -1642,6 +1737,14 @@
+
+
+
+
+
+
+
+
@@ -1650,6 +1753,14 @@
+
+
+
+
+
+
+
+
@@ -1658,6 +1769,14 @@
+
+
+
+
+
+
+
+
@@ -1879,6 +1998,11 @@
+
+
+
+
+
@@ -1897,6 +2021,14 @@
+
+
+
+
+
+
+
+
@@ -1979,6 +2111,14 @@
+
+
+
+
+
+
+
+
@@ -2003,11 +2143,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2310,6 +2463,14 @@
+
+
+
+
+
+
+
+
@@ -2350,6 +2511,14 @@
+
+
+
+
+
+
+
+
@@ -2361,6 +2530,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -2372,6 +2552,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -2383,6 +2574,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -2394,6 +2596,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -2405,6 +2618,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -2416,6 +2640,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -2427,6 +2662,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java
index dbfccc3..3d63321 100644
--- a/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java
+++ b/src/main/java/dev/toonformat/jtoon/decoder/ValueDecoder.java
@@ -102,9 +102,15 @@ private static Object decodeInternal(final String toon, final DecodeOptions opti
}
// Handle keyed arrays: items[2]{id,name}:
+ // Only match if the key part (before the bracket) doesn't contain a colon,
+ // because a colon indicates a key-value pair (e.g. 'a: "[2]: x"')
final Matcher keyedArray = KEYED_ARRAY_PATTERN.matcher(line);
if (keyedArray.matches()) {
- return KeyDecoder.parseKeyedArrayValue(keyedArray, line, depth, context);
+ final String keyPart = keyedArray.group(1);
+ final int colonInKey = DecodeHelper.findUnquotedColon(keyPart);
+ if (colonInKey <= 0) {
+ return KeyDecoder.parseKeyedArrayValue(keyedArray, line, depth, context);
+ }
}
// Handle key-value pairs: name: Ada
final int colonIdx = DecodeHelper.findUnquotedColon(line);
diff --git a/src/test/resources/conformance/decode/arrays-nested.json b/src/test/resources/conformance/decode/arrays-nested.json
index fad16f6..f66e6de 100644
--- a/src/test/resources/conformance/decode/arrays-nested.json
+++ b/src/test/resources/conformance/decode/arrays-nested.json
@@ -81,8 +81,7 @@
}
]
},
- "specSection": "10",
- "note": "Single-field list-item object: only the tabular array, no sibling fields"
+ "specSection": "10"
},
{
"name": "parses objects containing arrays (including empty arrays) in list format",
diff --git a/src/test/resources/conformance/decode/arrays-primitive.json b/src/test/resources/conformance/decode/arrays-primitive.json
index 7e948ba..63ff03f 100644
--- a/src/test/resources/conformance/decode/arrays-primitive.json
+++ b/src/test/resources/conformance/decode/arrays-primitive.json
@@ -107,6 +107,14 @@
},
"specSection": "9.1"
},
+ {
+ "name": "parses quoted key containing a colon with inline array",
+ "input": "\"a:b\"[2]: 1,2",
+ "expected": {
+ "a:b": [1, 2]
+ },
+ "specSection": "9.1"
+ },
{
"name": "parses quoted key with empty array",
"input": "\"x-custom\"[0]:",
diff --git a/src/test/resources/conformance/decode/arrays-tabular.json b/src/test/resources/conformance/decode/arrays-tabular.json
index 5adb1cc..636e2bc 100644
--- a/src/test/resources/conformance/decode/arrays-tabular.json
+++ b/src/test/resources/conformance/decode/arrays-tabular.json
@@ -80,6 +80,19 @@
"count": 2
},
"specSection": "9.3"
+ },
+ {
+ "name": "treats a key-value line at header depth whose value contains the active delimiter as end of rows, not a row",
+ "input": "t[2]{a,b}:\n 1,2\n 3,4\nx: 3,4",
+ "expected": {
+ "t": [
+ { "a": 1, "b": 2 },
+ { "a": 3, "b": 4 }
+ ],
+ "x": "3,4"
+ },
+ "specSection": "9.3",
+ "note": "The dedent to the header's depth closes the rows (§8); the line is a key-value pair, and the whole post-colon token is the value, non-numeric so a string (§11.2)"
}
]
}
diff --git a/src/test/resources/conformance/decode/delimiters.json b/src/test/resources/conformance/decode/delimiters.json
index 71191fd..021505b 100644
--- a/src/test/resources/conformance/decode/delimiters.json
+++ b/src/test/resources/conformance/decode/delimiters.json
@@ -159,7 +159,7 @@
"note": "a,b"
},
"specSection": "11",
- "note": "Object values don't require comma quoting regardless of delimiter"
+ "note": "Object field values are parsed as the whole post-colon token, never split on a delimiter (§11.2)"
},
{
"name": "object values in list items follow document delimiter",
diff --git a/src/test/resources/conformance/decode/numbers.json b/src/test/resources/conformance/decode/numbers.json
index a4d4e3c..d15cdac 100644
--- a/src/test/resources/conformance/decode/numbers.json
+++ b/src/test/resources/conformance/decode/numbers.json
@@ -86,8 +86,7 @@
"expected": {
"value": 5
},
- "specSection": "4",
- "note": "Exponent +00 results in the integer 5"
+ "specSection": "4"
},
{
"name": "parses zero with exponent as number",
diff --git a/src/test/resources/conformance/decode/objects.json b/src/test/resources/conformance/decode/objects.json
index 47a1b75..68b249e 100644
--- a/src/test/resources/conformance/decode/objects.json
+++ b/src/test/resources/conformance/decode/objects.json
@@ -122,6 +122,50 @@
},
"specSection": "8"
},
+ {
+ "name": "parses quoted object value shaped like an inline array header",
+ "input": "a: \"[2]: x\"",
+ "expected": {
+ "a": "[2]: x"
+ },
+ "specSection": "8",
+ "note": "Quoted value is opaque (§4); the bracket-and-colon shape inside the quotes is data, not an array header (§6)"
+ },
+ {
+ "name": "parses quoted object value shaped like a count-matching inline array header",
+ "input": "a: \"[1]: x\"",
+ "expected": {
+ "a": "[1]: x"
+ },
+ "specSection": "8",
+ "note": "Quoting makes the value opaque (§4), so it is never parsed as a header – the matching count is irrelevant because a quoted scalar is never a header candidate"
+ },
+ {
+ "name": "parses unquoted value shaped like an inline array header after the key",
+ "input": "key: foo [2]: bar",
+ "expected": {
+ "key": "foo [2]: bar"
+ },
+ "specSection": "6",
+ "note": "An unquoted key cannot contain a colon, so the first colon ends the key; the bracket-and-colon shape in the remainder is value text, not an array header"
+ },
+ {
+ "name": "parses unquoted value shaped like a tabular array header after the key",
+ "input": "a: b [2]{x,y}: 1,2",
+ "expected": {
+ "a": "b [2]{x,y}: 1,2"
+ },
+ "specSection": "6",
+ "note": "An unquoted key cannot contain a colon, so the first colon ends the key; the tabular-header shape in the remainder is value text, not an array header"
+ },
+ {
+ "name": "parses quoted object value with bracket text and a later colon",
+ "input": "content: \"accept (process.argv[2]) and greet: name\"",
+ "expected": {
+ "content": "accept (process.argv[2]) and greet: name"
+ },
+ "specSection": "8"
+ },
{
"name": "parses quoted key with colon",
"input": "\"order:id\": 7",
@@ -286,6 +330,14 @@
},
"specSection": "8"
},
+ {
+ "name": "unescapes tab in key immediately followed by a colon",
+ "input": "\"\\t:x\": v",
+ "expected": {
+ "\t:x": "v"
+ },
+ "specSection": "8"
+ },
{
"name": "unescapes quotes in key",
"input": "\"he said \\\"hi\\\"\": 1",
diff --git a/src/test/resources/conformance/decode/validation-errors.json b/src/test/resources/conformance/decode/validation-errors.json
index c525b46..a50b28b 100644
--- a/src/test/resources/conformance/decode/validation-errors.json
+++ b/src/test/resources/conformance/decode/validation-errors.json
@@ -185,6 +185,42 @@
"note": "[-1] is not a non-negative integer length; decoders MUST NOT interpret it as a bracket segment",
"minSpecVersion": "3.2"
},
+ {
+ "name": "throws on decimal bracket length in strict mode",
+ "input": "x[3.7]: a,b,c",
+ "expected": null,
+ "shouldError": true,
+ "options": {
+ "strict": true
+ },
+ "specSection": "6",
+ "note": "[3.7] is not an integer length (§6 length is DIGIT-only); decoders MUST NOT interpret it as a bracket segment",
+ "minSpecVersion": "3.2"
+ },
+ {
+ "name": "throws on bracket length with plus sign in strict mode",
+ "input": "x[+3]: a,b,c",
+ "expected": null,
+ "shouldError": true,
+ "options": {
+ "strict": true
+ },
+ "specSection": "6",
+ "note": "[+3] carries a sign and is not a non-negative integer length; decoders MUST NOT interpret it as a bracket segment",
+ "minSpecVersion": "3.2"
+ },
+ {
+ "name": "throws on bracket length in exponent form in strict mode",
+ "input": "x[1e1]: 1,2,3,4,5,6,7,8,9,10",
+ "expected": null,
+ "shouldError": true,
+ "options": {
+ "strict": true
+ },
+ "specSection": "6",
+ "note": "[1e1] is exponent notation, not a DIGIT-only integer length; decoders MUST NOT interpret it as a bracket segment",
+ "minSpecVersion": "3.2"
+ },
{
"name": "throws on whitespace between bracket segment and colon in strict mode",
"input": "items[2] :\n 1,2",
diff --git a/src/test/resources/conformance/decode/whitespace.json b/src/test/resources/conformance/decode/whitespace.json
index 7584e13..f2c51d2 100644
--- a/src/test/resources/conformance/decode/whitespace.json
+++ b/src/test/resources/conformance/decode/whitespace.json
@@ -37,8 +37,7 @@
{ "id": 2, "name": "Bob" }
]
},
- "specSection": "12",
- "note": "Values in tabular rows are trimmed"
+ "specSection": "12"
},
{
"name": "tolerates spaces around delimiters with quoted values",
diff --git a/src/test/resources/conformance/encode/arrays-objects.json b/src/test/resources/conformance/encode/arrays-objects.json
index 9646619..2688b85 100644
--- a/src/test/resources/conformance/encode/arrays-objects.json
+++ b/src/test/resources/conformance/encode/arrays-objects.json
@@ -59,7 +59,7 @@
},
"expected": "items[1]:\n - users[2]{id,name}:\n 1,Ada\n 2,Bob\n status: active",
"specSection": "10",
- "note": "YAML-style encoding for list-item objects with tabular array as first field"
+ "note": "Tabular header on hyphen line with rows at depth +2 and sibling fields at depth +1 (§10)"
},
{
"name": "uses list format for nested object arrays with mismatched keys",
diff --git a/src/test/resources/conformance/encode/key-folding.json b/src/test/resources/conformance/encode/key-folding.json
index 367732a..a96b34f 100644
--- a/src/test/resources/conformance/encode/key-folding.json
+++ b/src/test/resources/conformance/encode/key-folding.json
@@ -133,7 +133,7 @@
"flattenDepth": 0
},
"specSection": "13.4",
- "note": "flattenDepth=0 disables all folding"
+ "note": "flattenDepth below 2 has no practical effect, leaving standard nesting (§13.4)"
},
{
"name": "encodes standard nesting with keyFolding=off (baseline)",
diff --git a/src/test/resources/conformance/encode/primitives.json b/src/test/resources/conformance/encode/primitives.json
index da3b19b..373f536 100644
--- a/src/test/resources/conformance/encode/primitives.json
+++ b/src/test/resources/conformance/encode/primitives.json
@@ -63,7 +63,7 @@
"input": "05",
"expected": "\"05\"",
"specSection": "7.2",
- "note": "Leading zeros make it non-numeric"
+ "note": "Matches the numeric-like pattern (05 is a listed §7.2 example), so it MUST be quoted"
},
{
"name": "escapes newline in string",
@@ -198,8 +198,7 @@
"name": "encodes negative zero as zero",
"input": -0,
"expected": "0",
- "specSection": "2",
- "note": "Negative zero normalizes to zero"
+ "specSection": "2"
},
{
"name": "encodes scientific notation as decimal",
diff --git a/src/test/resources/conformance/encode/whitespace.json b/src/test/resources/conformance/encode/whitespace.json
index e370d59..27bb3a0 100644
--- a/src/test/resources/conformance/encode/whitespace.json
+++ b/src/test/resources/conformance/encode/whitespace.json
@@ -9,8 +9,7 @@
"id": 123
},
"expected": "id: 123",
- "specSection": "12",
- "note": "Output should not end with newline character"
+ "specSection": "12"
},
{
"name": "maintains proper indentation for nested structures",
@@ -37,8 +36,7 @@
"specSection": "12",
"options": {
"indent": 4
- },
- "note": "4-space indentation for nested objects when indent option is set to 4"
+ }
}
]
}