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
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
DetailsTiny 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
DetailsTiny 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
DetailsLightweight zero-dependency JSON Schema validator — fluent API, path-aware errors.
You want to define and validate JSON-like structures via a fluent API
DetailsLZ4 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
| Feature | femtolz4 | lz4-java |
|---|---|---|
| JAR size | ~50 KB | ~870 KB |
| Dependencies | None | None |
| Compression levels | 1–10 | 1–17 (incl. lz4opt) |
| Native platforms | linux/amd64, darwin/aarch64 | All major OS/arch |
| LZ4 frame format | ✓ | ✓ |
| Block API | ✓ | ✓ |
| xxHash-32 | ✓ | ✓ |
| API compatibility | Similar 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.
Related
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);
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);
}
}
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
| Feature | femtojson | Jackson |
|---|---|---|
| JAR size | ~13 KB | Large (core + modules) |
| Dependencies | None | Several |
| POJO databinding | ✗ | ✓ |
| Parse target | Map/List/primitives | POJOs, JsonNode, tokens |
| Streaming / token API | ✗ | ✓ |
| Pretty / compact print | ✓ | ✓ |
| Performance | Adequate | Very high |
| Maturity | Early | Extremely mature |
Pick femtojson for ad-hoc JSON without dependencies; pick Jackson for POJO binding, high throughput, or production reliability.
Related
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.
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
| Feature | femtoschema | json-schema-validator |
|---|---|---|
| JAR size | ~46 KB | Large |
| Dependencies | None | Jackson + others |
| JSON Schema Draft 2020-12 | Subset (export+import) | Full |
| $ref / allOf / anyOf / if-then | ✗ | ✓ |
| Fluent builder API | ✓ | ✗ |
| Discriminated unions | ✓ | ✗ |
| Path-aware error reporting | ✓ | ✓ |
| Validates plain Map/List trees | ✓ | Partial |
| Maturity | Early | Production-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).
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.