Somewhere around hour thirty of ZeroDepsHack 2026, we were staring at a parseUnicodeEscape method, arguing about whether a lone high surrogate followed by a regular character should throw at parse time or survive until serialization. The answer, per RFC 8259, was unambiguous — reject it — but the fact that we were even having the conversation said something about how much JSON parsing we'd been outsourcing to Jackson for years without thinking. That was the question the hackathon was built around. Not "are dependencies bad?" — they obviously aren't. But the supply chain keeps catching fire: left-pad breaking half the JS ecosystem over 11 lines in 2016, the chalk maintainer getting phished in 2025, npm's Shai-Hulud worm self-replicating through hundreds of packages, AI coding tools hallucinating package names that attackers pre-register. Jackson and Gson aren't the problem. The question is whether we still understand what's inside the box we'd normally just import. We decided to find out by building JValue — a zero-third-party-dependency JSON toolkit for Java 25 — during the Aug 28–31 build window, for Track B: Parsers & Data Formats. What We Reimplemented JValue shipped as a hand-written recursive-descent JSON parser, serializer, RFC 6901 JSON Pointer implementation, file convenience API, and CLI — 14 production source files, roughly 3000 lines of Java, all compiled with javac and depending on nothing beyond java.base. No Maven. No Gradle. No runtime dependency jar anywhere. We verified this at submission with jdeps, which confirmed every production class depended only on java.base. The public API surface we ended up with fit in a few lines: JsonValue doc = Json.parse(jsonString); String compact = Json.stringify(doc); String pretty = Json.stringifyPretty(doc); String name = Json.pointer(doc, "/users/0/name").asString(); Json.writePrettyFile(doc, Path.of("out.json")); JsonValue back = Json.read(Path.of("out.json")); The value model was a sealed interface hierarchy — JsonObject, JsonArray, JsonString, JsonNumber, JsonBoolean, JsonNull — which meant the compiler enforced exhaustive handling in switch expressions. That's something Jackson's JsonNode still doesn't give you: if you forget a case, javac tells you at compile time rather than letting it slip through to production. We also built a CLI with 15 commands — validate, pretty, compact, get (JSON Pointer lookup), inspect, roundtrip, build, array, numinfo, and more — all routed through a single switch expression on args[0] in JValueCli.java. No picocli. No Commons CLI. Just String[] args. What the Standard Library Made Painful Java's standard library is enormous and capable, but it was specifically missing two things that mattered for this project: a JSON implementation and a test framework. No JSON anything. Python ships json. Go ships encoding/json. Ruby ships json. Java ships... javax.json in Jakarta EE, which isn't in java.base and requires a runtime provider dependency. The entire reason Jackson exists is this gap. Building JValue meant writing a complete RFC 8259 parser, a serializer, string escape handling, Unicode processing, and number grammar enforcement from nothing but java.lang.String, java.lang.Character, and java.util.LinkedHashMap. No test framework. The JDK doesn't ship JUnit — it's a third-party dependency, which this hackathon didn't allow. We wrote a test harness from scratch: TestRunner.runTest(String, Runnable) wrapped each test, caught AssertionError for failures and Exception for errors, accumulated counts, and exited nonzero on failure. We wrote assertEquals, assertTrue, assertThrows, and the rest by hand. It worked. It was not fun. No parameterized test sugar, no IDE integration, no annotation discovery. You called your test methods from main() and you liked it. One specific annoyance: fetching external test corpora. We wanted to run JSONTestSuite's 318 conformance files, but git submodule and curl were external tools. We ended up writing FetchCorpus.java — a small JDK-only utility that used java.net.http.HttpClient to download the ZIP archive from GitHub, java.util.zip.ZipInputStream to extract it, and java.nio.file.Files to write the files out. It included zip-slip protection. It exited cleanly if offline so the build didn't break. It was the kind of thing you'd never write if curl | tar xz were on the table, but it worked, and it was zero-deps. The Package We Made Look Unnecessary Jackson (com.fasterxml.jackson). Specifically, the tree-model workflow: ObjectMapper.readTree() → traverse JsonNode → generate output. JValue replaced that vertical slice completely. Parse JSON text into a typed tree, navigate it, serialize it back — all without Jackson, Gson, or any third-party jar on the classpath. We weren't claiming Jackson is unnecessary in general. Jackson's streaming API, its POJO binding, its annotation system, its format modules — none of that was in JValue, and we didn't pretend it was. What we demonstrated was that the core tree-parse-serialize workflow — the reason most projects add Jackson in the first place — was buildable from the JDK in a weekend if you were willing to write it by hand. Our STDLIB.md tracked 12 specific substitutions with honest tradeoff documentation for each: Jackson's ObjectMapper → our recursive-descent parser. Jackson's JsonGenerator → our hand-written serializer. Guava's ImmutableMap → Collections.unmodifiableMap() wrapping LinkedHashMap. JUnit 5 → our test harness. AssertJ → hand-written assertions. ICU4J → java.lang.Character surrogate methods. picocli → String[] args switch expressions. Apache Commons IO → java.nio.file.Files. Each entry documented what we lost, not just what we gained. The Edge Case That Ate Real Time Unicode surrogate pairs. The JSON spec allows any Unicode code point in a string, but characters outside the Basic Multilingual Plane (above U+FFFF — emoji, musical symbols, historic scripts) must be encoded as a pair of \uXXXX escapes: a high surrogate (U+D800–U+DBFF) followed immediately by a low surrogate (U+DC00–U+DFFF). The parser had to match them, reject lone surrogates in either direction, and assemble the actual code point via Character.toCodePoint(). Here's the core of parseUnicodeEscape from JsonParser.java as it shipped: private char[] parseUnicodeEscape() { int codeUnit = parseHex4(); if (Character.isHighSurrogate((char) codeUnit)) { if (source.isAtEnd() || source.peek() != '\\') { throw source.error("High surrogate U+" + String.format("%04X", codeUnit) + " must be followed by a low surrogate"); } source.advance(); // consume '\' if (source.isAtEnd() || source.peek() != 'u') { throw source.error("High surrogate U+" + String.format("%04X", codeUnit) + " must be followed by \\uXXXX low surrogate"); } source.advance(); // consume 'u' int lowUnit = parseHex4(); if (!Character.isLowSurrogate((char) lowUnit)) { throw source.error("Expected low surrogate but found U+" + String.format("%04X", lowUnit)); } int codePoint = Character.toCodePoint( (char) codeUnit, (char) lowUnit); return Character.toChars(codePoint); } if (Character.isLowSurrogate((char) codeUnit)) { throw source.error("Unexpected low surrogate U+" + String.format("%04X", codeUnit) + " without preceding high surrogate"); } return new char[]{(char) codeUnit}; } The same strictness showed up on the serialization side — the serializer validated that Java strings didn't contain lone surrogates before emitting them, and rejected them with IllegalArgumentException. Both directions had to agree, or round-trip tests broke. We also burned real time on the number grammar. RFC 8259's number production looks trivial — [ minus ] int [ frac ] [ exp ] — but the details compounded: leading zeros were illegal (except bare 0), a trailing decimal point was invalid, +1 was invalid, hex notation was invalid, NaN and Infinity were invalid. We preserved the raw lexeme through parsing so that -0, 1.0, and 1e0 all round-tripped exactly as written — a property Jackson's JsonNode doesn't guarantee. What We Shipped By the Aug 31 code freeze: 186 hand-written tests, all passing. Zero failures, zero errors. 305 JSONTestSuite conformance cases passing, 0 failed, 13 skipped (the skipped cases were byte-level encoding tests that didn't apply to our String-based parser). 15 CLI commands — routed through a single switch expression on args[0] in JValueCli.java. Zero third-party production dependencies, confirmed by jdeps — every production class depended only on java.base. The CLI was the part that made the project feel like a real tool rather than just a library: $ ./jv.sh validate data.json ✔ Valid JSON Root type : object Keys : 3 $ ./jv.sh get data.json /users/0/name Pointer : /users/0/name Type : string Value : "Ada Lovelace" What We Learned Building a JSON parser from scratch turned out to be deeply educational and moderately miserable. We learned more about RFC 8259 in that weekend than we had in a decade of using Jackson. We also discovered that the spec was full of small traps — surrogate pairs, the exact set of legal whitespace characters, whether a leading BOM counts as valid input (we rejected it), whether trailing commas are allowed (they are not) — that libraries handle invisibly. If we're honest, the actual hard part of this weekend wasn't writing Unicode-handling code — most of that came together faster than either of us expected once the architecture was in place. The hard part was deciding how much to trust it. It's easy to generate a recursive-descent parser quickly now; it's much harder to know, with a submission deadline closing in, whether the thing you generated actually does what it claims. So most of our real effort in the last stretch went into verification rather than writing: running the parser against files we hadn't tested before and reading the output ourselves instead of trusting a green checkmark, deliberately breaking things to see if the errors pointed where they should, checking that a file written on one path and read back on another actually agreed. None of that shows up as a diff in the repo. If we had to hand one piece of advice to the next team building this way, it wouldn't be about surrogate pairs — it's that when an AI can generate the hard part in an afternoon, the bottleneck quietly moves to verifying it, and that's the part you can't delegate. We disclosed every tradeoff. Our STDLIB.md didn't just list substitutions; it documented what we lost — no POJO binding, no streaming, no configurable pretty-printing. Our README.md had a Limitations section. Overclaiming costs more than it gains when judges are senior engineers who will read the source. If we had the weekend back, there are things we'd explore — Reader/ InputStream parsing, streaming serialization, maybe JSON Patch. But the point of the project was never to replace Jackson wholesale. It was to answer a narrower question: for the core tree-parse-serialize workflow that most projects actually use Jackson for, do we still know how to build that ourselves? By submission, we had 186 passing tests, 305 conformance cases, and a jdeps report that said java.base and nothing else. The answer was yes. Built for ZeroDepsHack 2026 — Track B: Parsers & Data Formats. Organized by Hackathon Raptors. raptors.dev