JVM API Reference

import io.github.amaye15.jsontoolsrs.JsonTools;
import io.github.amaye15.jsontoolsrs.JsonToolsHandle;
import io.github.amaye15.jsontoolsrs.JsonToolsException;

Maven coordinates (published on tagged releases):

<dependency>
  <groupId>io.github.amaye15</groupId>
  <artifactId>json-tools-rs-spark</artifactId>
  <version>0.9.7</version>
</dependency>

The JVM bindings are a thin JNI shim (src/jvm.rs, built via the opt-in jvm Cargo feature) over the same Rust JSONTools core used by the Python bindings, with full feature parity. They're built primarily for use as Apache Spark UDFs -- see Setting Up on Databricks and jvm/README.md for the Spark-specific usage tiers (row UDF vs. batched mapPartitions transform). This page covers the underlying Java API those tiers are built on.

JsonTools

JsonTools is a pure-Java fluent builder mirroring the Rust JSONTools builder. It accumulates configuration in Java only; nothing crosses into native code until .build() is called. Unset fields are omitted from the serialized config entirely, so the underlying Rust JSONTools::new() defaults apply -- this class never hardcodes its own copy of a default value.

Construction

JsonTools builder = JsonTools.builder();

Operation Modes

Exactly one must be called before .build().

MethodDescription
.flatten()Flatten nested JSON into separator-delimited keys
.unflatten()Reconstruct nested JSON from flat, separator-delimited keys
.normal()Apply transformations without changing the nesting structure

Configuration Methods

All methods return this for chaining.

MethodTypeDefaultDescription
.separator(sep)String"."Key separator for flatten/unflatten
.lowercaseKeys(value)booleanfalseConvert all keys to lowercase
.removeEmptyStrings(value)booleanfalseFilter out "" values
.removeNulls(value)booleanfalseFilter out null values
.removeEmptyObjects(value)booleanfalseFilter out {} values
.removeEmptyArrays(value)booleanfalseFilter out [] values
.keyReplacement(find, replace)String, String--Add a key replacement pattern (literal by default, r'...' for regex); repeatable
.valueReplacement(find, replace)String, String--Add a value replacement pattern (literal by default, r'...' for regex); repeatable
.excludeKey(pattern)String--Drop any key (and its entire subtree) whose name contains pattern (literal by default, r'...' for regex); repeatable, additive
.excludeValue(pattern)String--Drop a key-value pair whose (scalar leaf) value contains pattern; repeatable, additive
.handleKeyCollision(value)booleanfalseCollect colliding keys into arrays
.autoConvertTypes(value)booleanfalseAuto-convert string values to native types (all 4 categories below, default behavior)
.convertDates(value)booleanfalseDate/datetime conversion, independently toggleable
.dateNormalizeToUtc(value)booleantrueNormalize recognized dates/datetimes to UTC; false leaves them unchanged
.dateAssumeUtcForNaive(value)booleantrueAppend Z to timezone-less datetimes; false leaves them unchanged
.convertNulls(value)booleanfalseNull-string conversion, independently toggleable
.nullExtraToken(token)String--Additional string recognized as null, beyond the built-in list; repeatable, additive
.convertBooleans(value)booleanfalseBoolean-string conversion, independently toggleable
.booleanExtraTrueToken(token) / .booleanExtraFalseToken(token)String--Additional true/false strings, beyond the built-in lists; repeatable, additive
.convertNumbers(value)booleanfalseNumeric-string conversion, independently toggleable
.numberCurrency(value)booleantrueCurrency symbol/code/credit-debit-suffix stripping
.numberPercent(value)booleantrue%/permille/per-ten-thousand suffix parsing
.numberBasisPoints(value)booleantrueText basis-point suffixes ("25bps")
.numberSuffixes(value)booleantrueK/M/B/T magnitude suffixes
.numberFractions(value)booleantrueFractions ("1/2")
.numberRadix(value)booleantrueHex/binary/octal literals ("0x1A")
.parallelThreshold(n)int100Min batch size for parallel processing
.numThreads(n)intCPU countThread count for parallelism
.nestedParallelThreshold(n)int100Min keys/items for intra-document parallelism
.maxArrayIndex(n)int100_000Max array index during unflattening (DoS protection)

.autoConvertTypes(value) only ever flips each category's on/off switch -- it never resets a category's customization (set via .dateNormalizeToUtc, .nullExtraToken, etc.) back to defaults, regardless of call order. Plain integers/decimals, scientific notation, and thousands-separator cleanup are always applied when .convertNumbers is enabled, regardless of the other number* settings. See Automatic Type Conversion for the full behavior reference (shared across all three language bindings).

Defaults for parallelThreshold, nestedParallelThreshold, numThreads, and maxArrayIndex can be overridden process-wide via the same environment variables as the Rust/Python bindings (JSON_TOOLS_PARALLEL_THRESHOLD, etc.) -- see Performance Tuning -- since they resolve on the Rust side regardless of which binding calls in.

See Key & Value Replacements for the r'...' literal-vs-regex convention, which applies identically here (patterns are passed through as plain strings and resolved entirely on the Rust side).

.build()

JsonToolsHandle build()

Serializes the accumulated configuration to a small, deterministic JSON blob (fixed field order, unset fields omitted) and constructs the native handle from it via one JNI call (nativeCreate). The caller owns the returned handle.

JsonToolsHandle

public final class JsonToolsHandle implements AutoCloseable

Owns one native JSONTools handle. The underlying Rust value has no interior mutability and is immutable after construction, so concurrent execute/executeBatch calls from multiple threads on the same handle are safe with no external locking.

MethodSignatureDescription
execute(json)String execute(String json)Process a single JSON document
executeBatch(jsonArray)String[] executeBatch(String[] jsonArray)Process a batch in one native call, using the same Rayon-parallel batch path as execute(Vec<String>) in Rust
close()void close()Frees the native handle. Using the handle afterward is undefined behavior.

A public constructor, JsonToolsHandle(String configJson), is also available for code that already has a raw config JSON string (e.g. the Spark BatchTransform tier, or a caller on the other side of the spark._jvm escape hatch) and doesn't need to go through the JsonTools fluent builder to get one.

try (JsonToolsHandle tools = JsonTools.builder()
        .flatten()
        .separator("::")
        .keyReplacement("r'^admin_'", "")
        .removeNulls(true)
        .build()) {
    String result = tools.execute("{\"admin_name\": \"Jane\", \"age\": null}");
    // result: {"name":"Jane"}

    String[] batch = tools.executeBatch(new String[] {
        "{\"admin_a\": 1}", "{\"admin_b\": 2}"
    });
}
// tools.close() is called automatically at the end of the try-with-resources block

close() is idempotent (safe to call more than once) and is the only supported way to free the native handle -- there is no finalizer. Use try-with-resources, as above, for a handle you own outright. Two lifecycle patterns are used internally for Spark UDFs that don't fit a simple try-with-resources scope: a process-wide cache of shared, never-closed handles for the row-UDF tier (NativeHandleCache), and one handle per partition closed via a Spark TaskContext completion listener for the batched mapPartitions tier (BatchTransform) -- see their source for the pattern if you're writing a custom UDF against this library.

Error Handling

public class JsonToolsException extends RuntimeException

Every native entry point is routed through a panic-catching guard on the Rust side: any error or panic in native code surfaces as a thrown JsonToolsException, never an unwind across the JNI boundary (which would be undefined behavior) and never a silent null/default return.

The exception's message is the Rust error's Display text, which embeds the same bracketed [E00x] machine-readable code used by the Rust and Python bindings -- see Error Codes for the full E001-E008 reference. Unlike the Python bindings (which prepend their own "Failed to process ...: " context before the code), the JVM message is the Rust Display text unmodified, so [E00x] is at the start of getMessage().

try (JsonToolsHandle tools = JsonTools.builder().flatten().build()) {
    String result = tools.execute("not valid json");
} catch (JsonToolsException e) {
    System.err.println(e.getMessage());
    // [E001] JSON parsing failed: ...
    if (e.getMessage().startsWith("[E001]")) {
        // handle a parse error specifically
    }
}

JsonToolsException is an unchecked (RuntimeException subclass) exception, so methods that call execute()/executeBatch()/build() don't need to declare it in a throws clause.

Complete Example

import io.github.amaye15.jsontoolsrs.JsonTools;
import io.github.amaye15.jsontoolsrs.JsonToolsHandle;
import io.github.amaye15.jsontoolsrs.JsonToolsException;

public class Example {
    public static void main(String[] args) {
        try (JsonToolsHandle tools = JsonTools.builder()
                .flatten()
                .separator("::")
                .lowercaseKeys(true)
                .removeNulls(true)
                .removeEmptyStrings(true)
                .keyReplacement("r'^user_'", "")
                .autoConvertTypes(true)
                .parallelThreshold(50)
                .numThreads(4)
                .build()) {

            // Single document
            String result = tools.execute("{\"User_Name\": \"Alice\", \"User_Age\": \"30\"}");

            // Batch
            String[] batch = tools.executeBatch(new String[] {
                "{\"a\": \"1\"}", "{\"b\": \"2\"}"
            });
        } catch (JsonToolsException e) {
            System.err.println("json-tools-rs error: " + e.getMessage());
        }
    }
}

For Spark-specific usage (row UDFs registered via spark.udf.registerJavaFunction, and the higher-throughput batched mapPartitions transform), see jvm/README.md and Setting Up on Databricks.