>_ Johannes Bechberger

femto libs

With AI we can finally build the tiny libraries we actually need — a JSON parser, a CLI framework, an LZ4 compressor — each containing exactly the features required, without the bloat and transitive dependencies of general-purpose solutions. These libraries are experiments in minimalism: small codebases, small JARs, simple APIs covering the common 90% use case.

Libraries

femtolz4 ready
v0.2.2 ·2026-08-25

LZ4 compression for Java — ~50 KB, zero dependencies, streaming + block API, lz4-java-inspired API for size-sensitive projects.

JAR size matters — Java agents, embedded tools, minimal distributions

Details
femtocli experimental
v0.4.4 ·2026-07-30

Tiny annotation-driven Java CLI framework — subcommands, options, <65 KB, no transitive deps.

You are building a CLI tool or Java agent where minimizing JAR size and transitive dependencies matters

Details
femtojson ready
v0.4.2 ·2026-04-10

Tiny zero-dependency JSON parser and pretty-printer — parse ad-hoc JSON without Jackson or Gson.

You need to parse or print JSON without pulling in a large dependency

Details
femtoschema poc
v0.1.2 ·2026-03-03

Lightweight zero-dependency JSON Schema validator — fluent API, path-aware errors.

You want to define and validate JSON-like structures via a fluent API

Details
femtojar experimental
v0.2.1 ·2026-04-30

Maven plugin that shrinks fat JARs via cross-class compression — 15–30% smaller, up to 70% with ProGuard.

You need the smallest possible executable (shaded/uber) JAR

Details
Libraries

femtolz4

ready v0.2.2 · 2026-08-25 GitHub

LZ4 frame compression for Java. Around 50 KB, zero transitive dependencies, full streaming and block API, pure Java with optional native acceleration on linux/amd64 and darwin/aarch64. Designed for Java agents, embedded tools, and minimal distributions where lz4-java's 870 KB is too heavy.

When to use

  • JAR size matters — Java agents, embedded tools, minimal distributions
  • You want lz4-java-inspired API (similar classes/methods, not a drop-in) with zero transitive deps
  • You need readSingleFrame support (e.g. reading CJFR files)
  • You only need linux/amd64 or darwin/aarch64 native acceleration

When not to use

  • You need native acceleration on Windows or non-amd64 Linux
  • You need lz4opt compression (lz4-java levels 11–17)
  • You need dependent-block frame encoding

vs lz4-java

Featurefemtolz4lz4-java
JAR size~50 KB~870 KB
DependenciesNoneNone
Compression levels1–101–17 (incl. lz4opt)
Native platformslinux/amd64, darwin/aarch64All major OS/arch
LZ4 frame format
Block API
xxHash-32
API compatibilitySimilar to lz4-java, not a drop-in

Pick femtolz4 when JAR size matters and you only need linux/mac — API is similar to lz4-java but not a drop-in replacement. Pick lz4-java for broad platform coverage, lz4opt levels, or when you need a true drop-in.

Install

<dependency>
  <groupId>me.bechberger</groupId>
  <artifactId>femtolz4</artifactId>
  <version>0.2.2</version>
</dependency>

Usage

try (var out = new LZ4FrameOutputStream(Files.newOutputStream(path))) {
    out.write(data);
}
try (var in = new LZ4FrameInputStream(Files.newInputStream(path))) {
    byte[] restored = in.readAllBytes();
}

Find more information at https://github.com/parttimenerd/femtolz4

How To

These examples may be incomplete or outdated — see the README / docs for the full reference.
Compress and decompress data with the streaming API

Wrap any OutputStream/InputStream — the LZ4 frame format handles all framing and checksums automatically:

import me.bechberger.femtolz4.LZ4FrameOutputStream;
import me.bechberger.femtolz4.LZ4FrameInputStream;
import java.nio.file.Files;
import java.nio.file.Path;

// Compress
try (var out = new LZ4FrameOutputStream(
        Files.newOutputStream(Path.of("data.lz4")))) {
    out.write(myBytes);
}

// Decompress
try (var in = new LZ4FrameInputStream(
        Files.newInputStream(Path.of("data.lz4")))) {
    byte[] restored = in.readAllBytes();
}

Default settings (level 1, 4 MiB blocks) give the best speed. See below for tuning compression level or block size.

Choose a compression level

Levels run from 1 (LEVEL_FAST, default) to 10 (LEVEL_OPTIMAL):

new LZ4FrameOutputStream(out)     // level 1 — fastest, good ratio (default)
new LZ4FrameOutputStream(out, 5)  // balanced
new LZ4FrameOutputStream(out, 9)  // best ratio, still fast (LEVEL_MAX / LEVEL_DEFAULT)
new LZ4FrameOutputStream(out, 10) // optimal parser, ~5–10× slower, +1.5–2.6% ratio over 9

The default 4 MiB block size works well for most cases. Use a smaller block (64 KiB, 256 KiB) to reduce peak memory, a larger one (4 MiB) to improve ratio on compressible data.

Read a single embedded LZ4 frame (e.g. CJFR files)

When an LZ4 frame is embedded inside a larger format with a non-LZ4 footer, use readSingleFrame=true to stop after the first frame and leave trailing bytes on the stream:

try (var in = new LZ4FrameInputStream(rawStream, true)) {
    byte[] frameData = in.readAllBytes();
}
// rawStream is now positioned immediately after the LZ4 end mark
Reuse a compressor handle to avoid hash-table allocation

For tight loops compressing many small buffers, reuse a Compressor handle so its internal hash table is not re-allocated on every call:

LZ4.Compressor c = LZ4.compressor(LZ4.LEVEL_FAST); // or any level
for (byte[] chunk : chunks) {
    try (var out = new LZ4FrameOutputStream(sink, c)) {
        out.write(chunk);
    }
}
Use the block API for custom binary formats

Use raw blocks when you manage block boundaries yourself and store the original size externally (no frame header overhead):

byte[] compressed = LZ4.compress(data);      // level 1 (fastest)
byte[] compressed = LZ4.compressHigh(data);  // level 9 (best ratio)

// Decompress — you must supply the original uncompressed size
byte[] original = LZ4.decompress(compressed, originalSize);

// Pre-allocate a destination buffer
int maxLen = LZ4.maxCompressedLength(data.length);
byte[] dst  = new byte[maxLen];
int len     = LZ4.compress(data, 0, data.length, dst, 0, LZ4.MAX_CHAIN);

femtocli

experimental v0.4.4 · 2026-07-30 GitHub

Minimal annotation-driven CLI framework for Java — subcommands, options, positional parameters, mixins, and built-in help/version in under 65 KB with no transitive dependencies. Includes a unique agent-args mode for parsing comma-separated Java agent argument strings.

When to use

  • You are building a CLI tool or Java agent where minimizing JAR size and transitive dependencies matters
  • You need agent-args mode — parsing comma-separated key=value strings like -javaagent:agent.jar=start,interval=1ms

When not to use

  • You need shell completion, interactive prompts, or localization

Install

<dependency>
  <groupId>me.bechberger.util</groupId>
  <artifactId>femtocli</artifactId>
  <version>0.4.4</version>
</dependency>

Usage

@Command(name = "myapp", subcommands = {GreetCmd.class})
public class App implements Runnable {
    public void run() {}
    public static void main(String[] args) { FemtoCli.run(new App(), args); }
}

Find more information at https://github.com/parttimenerd/femtocli

How To

These examples may be incomplete or outdated — see the README / docs for the full reference.
Define a command with subcommands and options

Commands implement Runnable or Callable<Integer>. Options and positional parameters are declared as annotated fields:

@Command(name = "greet", description = "Greet a person")
class GreetCmd implements Callable<Integer> {
    @Option(names = {"-n", "--name"}, description = "Name to greet", required = true)
    String name;

    @Option(names = {"-c", "--count"}, description = "Count (default: ${DEFAULT-VALUE})", defaultValue = "1")
    int count;

    @Override
    public Integer call() {
        for (int i = 0; i < count; i++) System.out.println("Hello, " + name + "!");
        return 0;
    }
}

@Command(name = "myapp", version = "1.0.0", subcommands = {GreetCmd.class})
public class App implements Runnable {
    public void run() {}
    public static void main(String[] args) { FemtoCli.run(new App(), args); }
}

Automatic -h/--help and -V/--version flags are added for free.

Define subcommands as methods

Annotate methods with @Command directly on the parent class — no separate class needed:

@Command(name = "myapp")
public class App implements Runnable {
    @Command(name = "status", description = "Show status")
    int status() {
        System.out.println("OK");
        return 0;
    }

    @Override
    public void run() {}

    public static void main(String[] args) { FemtoCli.run(new App(), args); }
}
Use agent-args mode for Java agents

Java agents receive a single comma-separated string (-javaagent:agent.jar=ARGS). Use FemtoCli.builder().runAgent(...) to parse it with full subcommand and option support:

// -javaagent:agent.jar=start,interval=1ms
// -javaagent:agent.jar=stop,output=recording.jfr,verbose
public static void premain(String agentArgs, Instrumentation inst) {
    FemtoCli.builder()
        .alertOnMixedStyleInAgent(true) // warn if user passes --opts instead of ,opts
        .runAgent(new MyAgent(), agentArgs);
}

alertOnMixedStyleInAgent detects when a user accidentally passes start --interval=1ms (space-separated) and suggests the correct comma-separated form.

Share options across subcommands with mixins

Define a class with shared options and inject it with @Mixin:

static class CommonOpts {
    @Option(names = {"-v", "--verbose"})
    boolean verbose;
}

@Command(name = "build")
static class BuildCmd implements Runnable {
    @Mixin CommonOpts common;

    public void run() {
        if (common.verbose) System.out.println("verbose build");
    }
}
Access parent command options from a subcommand

Declare a plain Spec field (injected automatically) and call spec.getParent(Class):

@Command(name = "root", subcommands = {Sub.class})
public class Root implements Runnable {
    @Option(names = "--config", defaultValue = "default.conf")
    String config;
    public void run() {}
}

@Command(name = "sub")
static class Sub implements Runnable {
    Spec spec;

    public void run() {
        Root root = spec.getParent(Root.class);
        System.out.println("config: " + root.config);
    }
}

femtojson

ready v0.4.2 · 2026-04-10 GitHub

Tiny JSON parser and pretty-printer for Java. Zero dependencies, straightforward Map/List API — no POJO binding, no annotations, just parse and go. For size-sensitive contexts (Java agents, embedded tools) where pulling in Jackson or Gson is too heavy.

When to use

  • You need to parse or print JSON without pulling in a large dependency
  • You work with ad-hoc JSON structures (Map/List trees), not POJOs
  • JAR size or transitive dependency count matters

When not to use

  • You need to bind JSON to Java objects (POJOs)
  • You need high-throughput JSON processing
  • You need streaming or token-level parsing

vs Jackson

FeaturefemtojsonJackson
JAR size~13 KBLarge (core + modules)
DependenciesNoneSeveral
POJO databinding
Parse targetMap/List/primitivesPOJOs, JsonNode, tokens
Streaming / token API
Pretty / compact print
PerformanceAdequateVery high
MaturityEarlyExtremely mature

Pick femtojson for ad-hoc JSON without dependencies; pick Jackson for POJO binding, high throughput, or production reliability.

Install

<dependency>
  <groupId>me.bechberger.util</groupId>
  <artifactId>femtojson</artifactId>
  <version>0.4.2</version>
</dependency>

Usage

try {
    Map<String, Object> obj = (Map<String, Object>)
        JSONParser.parse("{\"name\":\"Alice\",\"age\":30}");
    System.out.println(obj.get("name")); // Alice
    String pretty  = PrettyPrinter.prettyPrint(obj);
    String compact = PrettyPrinter.compactPrint(obj);
} catch (IOException e) { ... }

Find more information at https://github.com/parttimenerd/femtojson

How To

These examples may be incomplete or outdated — see the README / docs for the full reference.
Parse JSON into a Map/List tree

JSONParser.parse() returns a plain Java object — Map<String, Object> for objects, List<Object> for arrays, String, Double, Boolean, or null for scalars. All numbers come back as Double.

import me.bechberger.util.json.JSONParser;

Map<String, Object> user = (Map<String, Object>)
    JSONParser.parse("{\"name\": \"Alice\", \"age\": 30}");
String name = (String) user.get("name");   // "Alice"
double age  = (Double) user.get("age");    // 30.0

List<Object> tags = (List<Object>)
    JSONParser.parse("[\"java\", \"profiling\"]");

Throws IOException on malformed input — catch or declare it.

Pretty-print or compact-print a JSON value

Pass any parsed value (or a manually constructed Map/List) to PrettyPrinter:

import me.bechberger.util.json.PrettyPrinter;

Object value = JSONParser.parse(rawJson);

// Indented output (2 spaces per level)
String pretty  = PrettyPrinter.prettyPrint(value);

// Single-line output
String compact = PrettyPrinter.compactPrint(value);

All numbers are parsed as Double.

femtoschema

poc v0.1.2 · 2026-03-03 GitHub

Fluent JSON schema DSL and validator for Java. Zero dependencies. Define schemas programmatically, export/import JSON Schema Draft 2020-12 (subset), and get path-aware error messages. Companion to femtojson — adds lightweight structural validation without a full schema validator dependency.

When to use

  • You want to define and validate JSON-like structures via a fluent API
  • You need JSON Schema Draft 2020-12 interop (export/import, subset)
  • You are already using femtojson and want validation alongside it

When not to use

  • You need full JSON Schema spec compliance ($ref, allOf, anyOf, if/then)
  • You need to validate Jackson JsonNode trees
  • You need production-ready, battle-tested schema validation

vs json-schema-validator

Featurefemtoschemajson-schema-validator
JAR size~46 KBLarge
DependenciesNoneJackson + others
JSON Schema Draft 2020-12Subset (export+import)Full
$ref / allOf / anyOf / if-then
Fluent builder API
Discriminated unions
Path-aware error reporting
Validates plain Map/List treesPartial
MaturityEarlyProduction-ready

Pick femtoschema for programmatic schema definition with no dependencies; pick json-schema-validator for full spec compliance or external JSON Schema documents.

Related

Install

<dependency>
  <groupId>me.bechberger.util</groupId>
  <artifactId>femtoschema</artifactId>
  <version>0.1.2</version>
</dependency>

Usage

var schema = Schemas.object()
    .required("name", Schemas.string().withMinLength(1))
    .required("age",  Schemas.number().withMinimum(0));
ValidationResult r = schema.validate(Map.of("name","Alice","age",30.0));
r.getErrors().forEach(e -> System.out.println(e.path() + ": " + e.message()));

Find more information at https://github.com/parttimenerd/femtoschema

How To

These examples may be incomplete or outdated — see the README / docs for the full reference.
Define a schema and validate a value

Build a schema with Schemas.object(), declare required and optional fields, then call validate(). Path-aware errors tell you exactly which field failed:

import me.bechberger.util.femtoschema.Schemas;
import me.bechberger.util.femtoschema.ValidationResult;

var userSchema = Schemas.object()
    .required("name", Schemas.string().withMinLength(1))
    .required("age",  Schemas.number().withMinimum(0))
    .optional("email", Schemas.string());

ValidationResult r = userSchema.validate(
    Map.of("name", "", "age", -1.0)
);
r.getErrors().forEach(e ->
    System.out.println(e.path() + ": " + e.message())
);
// name: must have minimum length 1
// age: must be >= 0.0
Use enums and discriminated unions
// Enum — only the listed values are valid
var status = Schemas.enumOf("ACTIVE", "INACTIVE", "SUSPENDED");

// Discriminated union — dispatch on the "type" field
var notificationSchema = Schemas.sumType("type")
    .variant("email", Schemas.object()
        .required("type",    Schemas.enumOf("email"))
        .required("address", Schemas.string()))
    .variant("sms", Schemas.object()
        .required("type",        Schemas.enumOf("sms"))
        .required("phoneNumber", Schemas.string()));

notificationSchema.validate(
    Map.of("type", "email", "address", "alice@example.com")
).isValid(); // true
Export to JSON Schema and import back

Export any schema to a JSON Schema Draft 2020-12 Map (or string), then read it back — useful for sharing schemas with external tooling:

import me.bechberger.util.femtoschema.Schemas;
import me.bechberger.util.femtoschema.TypeSchema;

var schema = Schemas.object()
    .required("name", Schemas.string().withMinLength(1))
    .required("age",  Schemas.number().withMinimum(0));

// Export to JSON string
String json = Schemas.toJsonSchemaString(schema);

// Import back
TypeSchema imported = Schemas.fromJsonSchemaString(json);

Only the subset of keywords that toJsonSchema() produces is supported on import ($comment is silently ignored at any nesting level).

femtojar

experimental v0.2.1 · 2026-04-30 GitHub

Maven plugin and CLI that shrinks executable JARs via cross-class compression. Compresses all class files as a single blob instead of per-entry ZIP, enabling cross-class deduplication — typically 15–30% smaller. Optional ProGuard integration for up to 70% total size reduction.

When to use

  • You need the smallest possible executable (shaded/uber) JAR
  • You want optional ProGuard integration for maximum shrinkage

When not to use

  • You are packaging a library JAR (not an executable)
  • Build time is a hard constraint (Zopfli mode is slow)
  • Your code is reflection-heavy (picocli, serialization) — ProGuard needs extra keep rules

Related

Install

<plugin>
  <groupId>me.bechberger</groupId>
  <artifactId>femtojar</artifactId>
  <version>0.2.1</version>
  <executions>
    <execution>
      <goals><goal>reencode-jars</goal></goals>
    </execution>
  </executions>
</plugin>

Usage

mvn package
# Rewrites target/<artifactId>-<version>.jar in place by default

Find more information at https://github.com/parttimenerd/femtojar

How To

These examples may be incomplete or outdated — see the README / docs for the full reference.
Shrink a shaded JAR with the Maven plugin

Add femtojar after your shading plugin (e.g. maven-shade-plugin) so it runs on the already-assembled uber JAR:

<plugin>
  <groupId>me.bechberger</groupId>
  <artifactId>femtojar</artifactId>
  <version>VERSION</version>
  <executions>
    <execution>
      <goals><goal>reencode-jars</goal></goals>
    </execution>
  </executions>
</plugin>

Then run mvn package. femtojar rewrites target/<artifactId>-<version>.jar in place. The resulting JAR is a standard executable JAR — launch it with java -jar as normal.

Shrink a JAR with the standalone CLI

Build the CLI jar once with mvn package, then use it on any JAR without a Maven project:

java -jar femtojar-cli.jar app.jar                    # rewrite in place
java -jar femtojar-cli.jar app.jar app-optimized.jar  # write to separate file
java -jar femtojar-cli.jar app.jar app-optimized.jar --compression zopfli
java -jar femtojar-cli.jar app.jar app-optimized.jar --proguard --proguard-config proguard.conf
Enable ProGuard for maximum shrinkage

Add <proguard><enabled>true</enabled></proguard> to the plugin configuration. femtojar bundles a default ProGuard config (keep rules for main classes, native methods, enums, serialization, annotations). Always black-box test the result — ProGuard modifies bytecode and can break reflection-heavy code.

<configuration>
  <proguard>
    <enabled>true</enabled>
    <!-- optional: add extra keep rules -->
    <options>-dontobfuscate</options>
  </proguard>
</configuration>
Use Zopfli for maximum compression without ProGuard

ZOPFLI mode squeezes extra bytes by running more deflate iterations. Much slower than DEFAULT but requires no bytecode changes:

<configuration>
  <compressionMode>ZOPFLI</compressionMode>  <!-- or MAX for 100 iterations -->
</configuration>

ZOPFLI runs 7 iterations; MAX runs 100. Use DEFAULT (standard deflate) for fast builds.

Target a specific JAR or write to a separate output file

By default the plugin rewrites ${project.build.finalName}.jar in place. To target a different JAR or write to a separate path:

<configuration>
  <jars>
    <jar>
      <in>myapp-shaded.jar</in>
      <out>myapp-shaded-femto.jar</out>
    </jar>
  </jars>
</configuration>

Paths are relative to ${project.build.directory} unless absolute.

Always black-box test after enabling ProGuard — bytecode transformations can break edge cases.