Skip to content

Writing a Workalike

So you want a JSON Pointer library in another language that behaves like kPointer. The conformance suite is your acceptance test and the language-neutral spec. This page is the part the fixtures cannot give you: the translator's note — which layers to copy verbatim, which to make idiomatic, and the handful of Kotlin idioms in the reference that need a conscious decision in your target language.

It is short on purpose, and aimed squarely at you, six months from now, starting the port.

What to copy verbatim, what to make idiomatic

kPointer has two kinds of code, and they port differently.

Copy the syntax and algorithm logic verbatim. Pointer parsing, escaping, the relative-pointer grammar, resolve, and mutate are pure string-and-tree algorithms with no host dependency. Their behavior is normative and fully pinned by syntax/* and algorithm/*. Translate them line for line; resist "improving" them. Every deviation is a conformance risk.

Make the adapter layer idiomatic. The KpaStruct / KpaList / KpaPrimitive abstraction exists to bridge kPointer to some host tree type. In your language the natural bridge may look completely different — a trait, a protocol, a type class, a visitor, a tagged union you already have. Do not transliterate the Kotlin interface hierarchy; re-express the same contract (read a key, read an index, absent-vs-error, rebuild on mutate) in whatever your host considers idiomatic. The conformance fixtures test the contract, not the interface shape.

The dividing line: if a fixture pins it, copy it; if only the reference's internal structure implies it, redesign it.

Kotlin idioms that need a conscious mapping

Sealed interface → discriminated union

KpaElement is a sealed interface with exactly three cases (KpaStruct, KpaList, KpaPrimitive); RelativePointerResult is sealed with Pointer / Index / Key; the error taxonomies are sealed families. "Sealed" means closed set, exhaustively matchable.

Map each sealed hierarchy to your language's closed sum type:

  • Rust / Swift → enum with associated data.
  • TypeScript → a discriminated union with a type/kind tag (the fixtures already use exactly this shape for elements).
  • A language without sum types → an abstract base plus a kind discriminant, and a match/switch that a reviewer can see is exhaustive.

The payoff is the same as in Kotlin: resolve and mutate are written as exhaustive matches over the element kind, and adding a case is a compile-time prompt to handle it everywhere.

Poko / structural equality → explicit deep compare

The reference relies on structural value equality. KPointer and friends are Poko types (kPointer's stand-in for data class), so == compares by content, and the fixture element/document comparisons are deep structural matches (see element matching).

If your language's == is reference equality by default (Java without records, JavaScript objects, many others), you must write the deep compare yourself and route every "does this element equal that one" through it. A shallow or reference compare will pass the trivial fixtures and fail the nested-struct and nested-list cases in confusing ways. Decide your equality strategy before you implement resolve/mutate, not after.

Numeric and character decisions

Two decisions the fixtures depend on, both easy to get subtly wrong:

  • long versus double are distinct. The element type system tags integers (long) and floating-point (double) separately, precisely because JSON — and single-numeric-type languages like JavaScript — blur them. Preserve the distinction end to end: a long fixture must not round-trip as a double. If your host has one number type, carry the tag alongside the value.
  • Parse digits as ASCII only. Segment-index and relative-pointer integers are parsed strictly as ASCII 09. Do not use a locale-aware or Unicode-digit parser: strings containing non-ASCII digits (e.g. Arabic-Indic or full-width numerals) must be rejected, not silently accepted. The reference restricts this deliberately, and error-relative-non-ascii-digit pins it. Many standard-library integer parsers accept Unicode digits by default — check yours, and constrain it.

Relatedly, leading-zero and empty-integer rules ("01" rejected, no-leading-digit rejected) are part of the grammar, not the number parser — enforce them in the grammar as the reference does, so they hold regardless of how permissive your integer parser is.

Suggested porting order

  1. Pointer syntax (syntax/parsing.json) — parse/escape/emit for RFC 6901, fragment, and (optionally) dot notation. Get the escaping edge cases right here.
  2. Relative pointers (syntax/relative-apply.json, syntax/relative-compute.json) — the grammar, # queries, and index adjustments.
  3. Resolve (algorithm/resolve.json) — needs your idiomatic tree abstraction and the absent-vs-error rule.
  4. Mutate (algorithm/mutate.json) — non-destructive set/remove, rebuilding containers.

Wire each fixture file into your test framework as you go, keyed on the fixture name. When all five files are green you have a faithful workalike. The coverage matrix maps every RFC 6901 edge case to the fixture that covers it.