My previous Python config validator was great for local development, but it hit a wall in minimal "distroless" containers and hardened environments where Python isn't much of an option.
Sometimes you can't control the environment; you can only control the tool. To ensure our validation logic could run anywhere from a CI runner to a bare-bones production box, I rewrote the tool in Java. This version focuses on portability, zero external dependencies, and "fail-fast" logic.
Why a Java version?
A few reasons kept coming up:
Zero external dependencies: no pip, no venv, no system packages
Easy to bundle: one JAR or native image
Runs anywhere: CI, containers, internal tooling
Teams already familiar with JVM tooling
The logic is the same as the Python version:
load config, check structure, fail fast.
The difference is the runtime assumptions.
The CLI structure
The tool is intentionally small:
ConfigLoader: loads YAML/JSON.
Validator: checks required keys and types
HostValidator: regex validation for hostnames
DatabaseValidator: nested key checks
Main: CLI entrypoint
Nothing fancy. No frameworks, just enough structure to keep it readable.
Loading YAML or JSON in Java
I kept the loader simple. I'm using Jackson (jackson-databind and jackson-dataformat-yaml) to handle the parsing logic:
ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
ObjectMapper jsonMapper = new ObjectMapper();
public Map<String, Object> loadConfig(Path path) throws IOException {
if (!Files.exists(path)) {
throw new FileNotFoundException("Config file not found: " + path);
}
String text = Files.readString(path);
if (path.toString().endsWith(".yaml") || path.toString().endsWith(".yml")) {
return yamlMapper.readValue(text, Map.class);
} else if (path.toString().endsWith(".json")) {
return jsonMapper.readValue(text, Map.class);
}
throw new IllegalArgumentException("Unsupported file type: " + path);
}
Same behavior as the Python version, just typed differently.
Required keys and type checking
Java doesn’t have Python’s dynamic feel, so I defined expected types like this instead:
Map<String, Class<?>> REQUIRED_KEYS = Map.of(
"service_name", String.class,
"port", Integer.class,
"debug", Boolean.class,
"allowed_hosts", List.class
);
And the validator walks through them:
List<String> validate(Map<String, Object> cfg) {
List<String> errors = new ArrayList<>();
for (var entry : REQUIRED_KEYS.entrySet()) {
String key = entry.getKey();
Class<?> expected = entry.getValue();
if (!cfg.containsKey(key)) {
errors.add("Missing required key: '" + key + "'");
continue;
}
Object value = cfg.get(key);
if (!expected.isInstance(value)) {
errors.add("Invalid type for '" + key + "': expected "
+ expected.getSimpleName() + ", got "
+ value.getClass().getSimpleName());
}
}
return errors;
}
Hostname validation
Same regex, same idea:
Pattern HOST_REGEX = Pattern.compile("^[a-zA-Z0-9.-]+$");
void validateHosts(Map<String, Object> cfg, List<String> errors) {
Object hosts = cfg.get("allowed_hosts");
if (!(hosts instanceof List<?> list)) return;
for (Object h : list) {
if (!(h instanceof String s) || !HOST_REGEX.matcher(s).matches()) {
errors.add("Invalid host value: '" + h + "'");
}
}
}
Nested database validation
void validateDatabase(Map<String, Object> cfg, List<String> errors) {
Object dbObj = cfg.get("database");
if (dbObj == null) return;
if (!(dbObj instanceof Map<?, ?> db)) {
errors.add("Invalid type for 'database': expected Map");
return;
}
if (!db.containsKey("host")) {
errors.add("Missing 'database.host'");
}
if (!db.containsKey("port")) {
errors.add("Missing 'database.port'");
} else if (!(db.get("port") instanceof Integer)) {
errors.add("Invalid type for 'database.port'");
}
}
Same checks, different language.
Running it
The CLI is uncomplicated:
java -jar validator.jar config.yaml
If something’s wrong, it prints errors and exits with a non‑zero code.
If everything’s fine, it keeps quiet.
That’s all there is to it.
Why this matters
What’s your 'go-to' language when you need a tool to run absolutely anywhere? Do you stick with the JVM, or have you moved toward Go/Rust for these types of CLI tools? I'd love to hear about the constraints you're working with.
Top comments (0)