Jsonnet for Configuration in Java: Advanced Configuration Management

Jsonnet is a powerful configuration language that extends JSON with features like variables, functions, and imports. It helps manage complex configurations for Java applications by providing templating, composition, and abstraction capabilities.


Core Concepts

What is Jsonnet?

  • A data templating language that extends JSON
  • Adds variables, functions, conditionals, and imports
  • Generates JSON, YAML, or other structured data
  • Ideal for complex configuration management

Key Features:

  • Variables and References: Reuse values across configuration
  • Functions: Parameterized configuration snippets
  • Imports: Modular configuration composition
  • Conditionals: Environment-specific configuration
  • Arithmetic and String Operations: Dynamic value computation

Dependencies and Setup

1. Maven Dependencies
<properties>
<jsonnet.version>0.20.0</jsonnet.version>
<jackson.version>2.15.2</jackson.version>
<spring-boot.version>3.1.0</spring-boot.version>
</properties>
<dependencies>
<!-- Jsonnet Java Implementation -->
<dependency>
<groupId>org.jsonnet</groupId>
<artifactId>jsonnet</artifactId>
<version>${jsonnet.version}</version>
</dependency>
<!-- Jackson for JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Spring Boot (optional) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring-boot.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
2. Jsonnet Installation (CLI)
# Install Jsonnet CLI (for development/testing)
# Using Go
go get github.com/google/go-jsonnet/cmd/jsonnet
# Or download binary
wget https://github.com/google/go-jsonnet/releases/download/v0.20.0/go-jsonnet_0.20.0_Linux_x86_64.tar.gz
tar -xzf go-jsonnet_0.20.0_Linux_x86_64.tar.gz
sudo mv jsonnet /usr/local/bin/

Core Implementation

1. Jsonnet Evaluation Service
@Service
@Slf4j
public class JsonnetEvaluationService {
private final JsonnetEngine jsonnetEngine;
private final ObjectMapper jsonMapper;
private final ObjectMapper yamlMapper;
public JsonnetEvaluationService() {
this.jsonnetEngine = new JsonnetEngine();
this.jsonMapper = new ObjectMapper();
this.yamlMapper = new ObjectMapper(new YAMLFactory());
this.yamlMapper.findAndRegisterModules();
}
public JsonNode evaluateJsonnet(String jsonnetCode, Map<String, Object> extVars) {
try {
log.debug("Evaluating Jsonnet code with {} external variables", extVars.size());
// Configure Jsonnet engine
JsonnetConfig config = new JsonnetConfig();
config.setMaxStack(200);
config.setGcMinObjects(1000);
config.setMaxTrace(20);
// Add external variables
if (extVars != null) {
extVars.forEach((key, value) -> {
if (value instanceof String) {
config.addExtVar(key, (String) value);
} else if (value instanceof Number) {
config.addExtVar(key, value.toString());
} else if (value instanceof Boolean) {
config.addExtVar(key, value.toString());
} else {
// Convert complex objects to JSON strings
try {
String jsonValue = jsonMapper.writeValueAsString(value);
config.addExtVar(key, jsonValue);
} catch (JsonProcessingException e) {
log.warn("Failed to serialize external variable {}: {}", key, e.getMessage());
}
}
});
}
// Evaluate Jsonnet
String jsonResult = jsonnetEngine.evaluateSnippet("config", jsonnetCode, config);
// Parse result as JSON node
return jsonMapper.readTree(jsonResult);
} catch (JsonnetException e) {
log.error("Jsonnet evaluation failed: {}", e.getMessage());
throw new JsonnetEvaluationException("Failed to evaluate Jsonnet: " + e.getMessage(), e);
} catch (JsonProcessingException e) {
log.error("Failed to parse Jsonnet result: {}", e.getMessage());
throw new JsonnetEvaluationException("Failed to parse evaluation result", e);
}
}
public <T> T evaluateJsonnet(String jsonnetCode, Map<String, Object> extVars, Class<T> valueType) {
JsonNode result = evaluateJsonnet(jsonnetCode, extVars);
try {
return jsonMapper.treeToValue(result, valueType);
} catch (JsonProcessingException e) {
log.error("Failed to convert Jsonnet result to type {}: {}", valueType.getSimpleName(), e.getMessage());
throw new JsonnetEvaluationException("Failed to convert result to target type", e);
}
}
public String evaluateToYaml(String jsonnetCode, Map<String, Object> extVars) {
JsonNode result = evaluateJsonnet(jsonnetCode, extVars);
try {
return yamlMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
log.error("Failed to convert Jsonnet result to YAML: {}", e.getMessage());
throw new JsonnetEvaluationException("Failed to convert result to YAML", e);
}
}
public JsonNode evaluateFile(String filePath, Map<String, Object> extVars) {
try {
String jsonnetCode = Files.readString(Paths.get(filePath));
return evaluateJsonnet(jsonnetCode, extVars);
} catch (IOException e) {
log.error("Failed to read Jsonnet file {}: {}", filePath, e.getMessage());
throw new JsonnetEvaluationException("Failed to read Jsonnet file: " + filePath, e);
}
}
public JsonNode evaluateFileWithImports(String filePath, Map<String, Object> extVars, 
List<String> importPaths) {
try {
JsonnetConfig config = new JsonnetConfig();
// Add import paths
if (importPaths != null) {
importPaths.forEach(config::addJpath);
}
// Add external variables
if (extVars != null) {
extVars.forEach((key, value) -> {
if (value instanceof String) {
config.addExtVar(key, (String) value);
} else {
try {
String jsonValue = jsonMapper.writeValueAsString(value);
config.addExtVar(key, jsonValue);
} catch (JsonProcessingException e) {
log.warn("Failed to serialize external variable {}: {}", key, e.getMessage());
}
}
});
}
String jsonResult = jsonnetEngine.evaluateFile(filePath, config);
return jsonMapper.readTree(jsonResult);
} catch (JsonnetException e) {
log.error("Jsonnet file evaluation failed: {}", e.getMessage());
throw new JsonnetEvaluationException("Failed to evaluate Jsonnet file: " + filePath, e);
} catch (JsonProcessingException e) {
log.error("Failed to parse Jsonnet result: {}", e.getMessage());
throw new JsonnetEvaluationException("Failed to parse evaluation result", e);
}
}
public ValidationResult validateJsonnet(String jsonnetCode) {
try {
// Try to evaluate with empty variables
evaluateJsonnet(jsonnetCode, Map.of());
return ValidationResult.valid();
} catch (JsonnetEvaluationException e) {
return ValidationResult.invalid(e.getMessage());
}
}
}
// Custom exception for Jsonnet evaluation errors
public class JsonnetEvaluationException extends RuntimeException {
public JsonnetEvaluationException(String message) {
super(message);
}
public JsonnetEvaluationException(String message, Throwable cause) {
super(message, cause);
}
}
2. Configuration Template Service
@Service
@Slf4j
public class JsonnetTemplateService {
private final JsonnetEvaluationService evaluationService;
private final ObjectMapper jsonMapper;
public JsonnetTemplateService(JsonnetEvaluationService evaluationService) {
this.evaluationService = evaluationService;
this.jsonMapper = new ObjectMapper();
}
public ApplicationConfig generateApplicationConfig(String environment, 
ApplicationProfile profile) {
log.info("Generating application config for environment: {}, profile: {}", 
environment, profile.getName());
// Build external variables for template
Map<String, Object> extVars = buildExternalVariables(environment, profile);
// Evaluate main configuration template
String jsonnetTemplate = loadTemplate("templates/application.jsonnet");
JsonNode configNode = evaluationService.evaluateJsonnet(jsonnetTemplate, extVars);
try {
return jsonMapper.treeToValue(configNode, ApplicationConfig.class);
} catch (JsonProcessingException e) {
log.error("Failed to parse application config: {}", e.getMessage());
throw new JsonnetEvaluationException("Failed to parse application configuration", e);
}
}
public KubernetesManifest generateKubernetesManifest(String appName, 
String environment,
DeploymentSpec spec) {
log.info("Generating Kubernetes manifest for: {} in {}", appName, environment);
Map<String, Object> extVars = new HashMap<>();
extVars.put("appName", appName);
extVars.put("environment", environment);
extVars.put("replicas", spec.getReplicas());
extVars.put("image", spec.getImage());
extVars.put("version", spec.getVersion());
extVars.put("resources", spec.getResources());
String jsonnetTemplate = loadTemplate("templates/kubernetes.jsonnet");
JsonNode manifestNode = evaluationService.evaluateJsonnet(jsonnetTemplate, extVars);
try {
return jsonMapper.treeToValue(manifestNode, KubernetesManifest.class);
} catch (JsonProcessingException e) {
log.error("Failed to parse Kubernetes manifest: {}", e.getMessage());
throw new JsonnetEvaluationException("Failed to parse Kubernetes manifest", e);
}
}
public MultiEnvironmentConfig generateMultiEnvironmentConfig(EnvironmentMatrix matrix) {
log.info("Generating multi-environment configuration");
Map<String, Object> extVars = new HashMap<>();
extVars.put("environments", matrix.getEnvironments());
extVars.put("applications", matrix.getApplications());
extVars.put("defaults", matrix.getDefaults());
String jsonnetTemplate = loadTemplate("templates/multi-env.jsonnet");
JsonNode configNode = evaluationService.evaluateJsonnet(jsonnetTemplate, extVars);
try {
return jsonMapper.treeToValue(configNode, MultiEnvironmentConfig.class);
} catch (JsonProcessingException e) {
log.error("Failed to parse multi-environment config: {}", e.getMessage());
throw new JsonnetEvaluationException("Failed to parse multi-environment configuration", e);
}
}
public String renderTemplate(String templateName, Map<String, Object> parameters) {
String jsonnetTemplate = loadTemplate("templates/" + templateName + ".jsonnet");
JsonNode result = evaluationService.evaluateJsonnet(jsonnetTemplate, parameters);
try {
return jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
} catch (JsonProcessingException e) {
log.error("Failed to render template {}: {}", templateName, e.getMessage());
throw new JsonnetEvaluationException("Failed to render template: " + templateName, e);
}
}
private Map<String, Object> buildExternalVariables(String environment, ApplicationProfile profile) {
Map<String, Object> extVars = new HashMap<>();
extVars.put("env", environment);
extVars.put("profile", profile.getName());
extVars.put("datasource", buildDataSourceConfig(environment, profile));
extVars.put("redis", buildRedisConfig(environment, profile));
extVars.put("security", buildSecurityConfig(environment, profile));
extVars.put("logging", buildLoggingConfig(environment, profile));
extVars.put("features", profile.getFeatureFlags());
return extVars;
}
private Map<String, Object> buildDataSourceConfig(String environment, ApplicationProfile profile) {
Map<String, Object> dsConfig = new HashMap<>();
// Database configuration based on environment
switch (environment.toLowerCase()) {
case "production":
dsConfig.put("url", "jdbc:postgresql://prod-db.company.com:5432/appdb");
dsConfig.put("username", "app_user");
dsConfig.put("maxPoolSize", 20);
break;
case "staging":
dsConfig.put("url", "jdbc:postgresql://staging-db.company.com:5432/appdb");
dsConfig.put("username", "staging_user");
dsConfig.put("maxPoolSize", 10);
break;
default: // development
dsConfig.put("url", "jdbc:postgresql://localhost:5432/appdb");
dsConfig.put("username", "dev_user");
dsConfig.put("maxPoolSize", 5);
}
return dsConfig;
}
private Map<String, Object> buildRedisConfig(String environment, ApplicationProfile profile) {
Map<String, Object> redisConfig = new HashMap<>();
switch (environment.toLowerCase()) {
case "production":
redisConfig.put("host", "redis-cluster.company.com");
redisConfig.put("port", 6379);
redisConfig.put("cluster", true);
break;
default:
redisConfig.put("host", "localhost");
redisConfig.put("port", 6379);
redisConfig.put("cluster", false);
}
return redisConfig;
}
private Map<String, Object> buildSecurityConfig(String environment, ApplicationProfile profile) {
Map<String, Object> securityConfig = new HashMap<>();
securityConfig.put("enabled", !"development".equals(environment));
securityConfig.put("jwtSecret", profile.getSecrets().get("jwtSecret"));
securityConfig.put("corsOrigins", profile.getCorsOrigins());
return securityConfig;
}
private Map<String, Object> buildLoggingConfig(String environment, ApplicationProfile profile) {
Map<String, Object> loggingConfig = new HashMap<>();
loggingConfig.put("level", "production".equals(environment) ? "INFO" : "DEBUG");
loggingConfig.put("file", "production".equals(environment) ? "/var/log/app.log" : "app.log");
loggingConfig.put("maxSize", "100MB");
loggingConfig.put("maxHistory", 30);
return loggingConfig;
}
private String loadTemplate(String templatePath) {
try {
// Load from classpath
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(templatePath);
if (inputStream != null) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
// Fallback to file system
return Files.readString(Paths.get(templatePath));
} catch (IOException e) {
log.error("Failed to load template {}: {}", templatePath, e.getMessage());
throw new JsonnetEvaluationException("Template not found: " + templatePath, e);
}
}
}
3. Configuration Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApplicationConfig {
private String name;
private String version;
private String environment;
private ServerConfig server;
private DatabaseConfig database;
private RedisConfig redis;
private SecurityConfig security;
private LoggingConfig logging;
private Map<String, Object> features;
private List<ExternalServiceConfig> externalServices;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ServerConfig {
private int port;
private String contextPath;
private int maxHttpHeaderSize;
private SSLConfig ssl;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class DatabaseConfig {
private String url;
private String username;
private String password;
private String driverClassName;
private int maxPoolSize;
private int minIdle;
private long connectionTimeout;
private boolean showSql;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class RedisConfig {
private String host;
private int port;
private String password;
private int database;
private long timeout;
private boolean cluster;
private List<String> clusterNodes;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SecurityConfig {
private boolean enabled;
private String jwtSecret;
private long jwtExpiration;
private List<String> corsOrigins;
private List<String> permittedPaths;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class LoggingConfig {
private String level;
private String file;
private String maxSize;
private int maxHistory;
private Map<String, String> loggers;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SSLConfig {
private boolean enabled;
private String keyStore;
private String keyStorePassword;
private String keyAlias;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ExternalServiceConfig {
private String name;
private String baseUrl;
private int timeout;
private int maxRetries;
private AuthenticationConfig authentication;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class AuthenticationConfig {
private String type; // BASIC, OAUTH2, API_KEY
private String username;
private String password;
private String token;
private String apiKey;
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApplicationProfile {
private String name;
private String description;
private Map<String, Object> featureFlags;
private Map<String, String> secrets;
private List<String> corsOrigins;
private Map<String, Object> customProperties;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class KubernetesManifest {
private Map<String, Object> deployment;
private Map<String, Object> service;
private Map<String, Object> configMap;
private Map<String, Object> secret;
private Map<String, Object> ingress;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DeploymentSpec {
private String appName;
private String environment;
private String image;
private String version;
private int replicas;
private ResourceSpec resources;
private Map<String, String> environmentVariables;
private Map<String, String> labels;
private Map<String, String> annotations;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ResourceSpec {
private String memoryRequest;
private String memoryLimit;
private String cpuRequest;
private String cpuLimit;
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EnvironmentMatrix {
private List<String> environments;
private List<String> applications;
private Map<String, Object> defaults;
private Map<String, Map<String, Object>> overrides;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MultiEnvironmentConfig {
private Map<String, ApplicationConfig> environments;
private Map<String, Object> shared;
private Map<String, Object> defaults;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ValidationResult {
private boolean valid;
private String message;
private List<String> errors;
public static ValidationResult valid() {
return ValidationResult.builder()
.valid(true)
.message("Validation successful")
.errors(List.of())
.build();
}
public static ValidationResult invalid(String error) {
return ValidationResult.builder()
.valid(false)
.message("Validation failed")
.errors(List.of(error))
.build();
}
public static ValidationResult invalid(List<String> errors) {
return ValidationResult.builder()
.valid(false)
.message("Validation failed with " + errors.size() + " errors")
.errors(errors)
.build();
}
}

Jsonnet Template Examples

1. Application Configuration Template
// templates/application.jsonnet
local env = std.extVar("env");
local profile = std.extVar("profile");
local datasource = std.extVar("datasource");
local redis = std.extVar("redis");
local security = std.extVar("security");
local logging = std.extVar("logging");
local features = std.extVar("features");
// Base configuration
local baseConfig = {
name: "my-application",
version: "1.0.0",
environment: env,
server: {
port: 8080,
contextPath: "/api",
maxHttpHeaderSize: 16384,
ssl: if env == "production" then {
enabled: true,
keyStore: "/app/keystore.p12",
keyStorePassword: std.extVar("sslPassword"),
keyAlias: "tomcat"
} else {
enabled: false
}
},
database: {
url: datasource.url,
username: datasource.username,
password: std.extVar("dbPassword"),
driverClassName: "org.postgresql.Driver",
maxPoolSize: datasource.maxPoolSize,
minIdle: 2,
connectionTimeout: 30000,
showSql: env == "development"
},
redis: {
host: redis.host,
port: redis.port,
password: std.extVar("redisPassword"),
database: 0,
timeout: 2000,
cluster: redis.cluster,
clusterNodes: if redis.cluster then [
redis.host + ":6379",
redis.host + ":6380",
redis.host + ":6381"
] else []
},
security: {
enabled: security.enabled,
jwtSecret: security.jwtSecret,
jwtExpiration: 86400000, // 24 hours
corsOrigins: security.corsOrigins,
permittedPaths: [
"/api/public/**",
"/actuator/health",
"/actuator/info"
]
},
logging: {
level: logging.level,
file: logging.file,
maxSize: logging.maxSize,
maxHistory: logging.maxHistory,
loggers: {
"com.example": if env == "development" then "DEBUG" else "INFO",
"org.springframework": "WARN",
"org.hibernate": "WARN"
}
},
features: features,
externalServices: [
{
name: "payment-service",
baseUrl: if env == "production" then 
"https://payment.api.company.com" else
"https://payment.staging.company.com",
timeout: 5000,
maxRetries: 3,
authentication: {
type: "API_KEY",
apiKey: std.extVar("paymentApiKey")
}
},
{
name: "notification-service",
baseUrl: if env == "production" then 
"https://notifications.company.com" else
"https://notifications.staging.company.com",
timeout: 3000,
maxRetries: 2,
authentication: {
type: "OAUTH2",
token: std.extVar("notificationToken")
}
}
]
};
// Environment-specific overrides
local environmentOverrides = {
development: {
server: {
port: 8080
},
database: {
showSql: true
},
logging: {
level: "DEBUG"
}
},
staging: {
server: {
port: 8080
},
database: {
showSql: false
}
},
production: {
server: {
port: 8443
},
logging: {
level: "INFO",
file: "/var/log/application.log"
}
}
};
// Merge base config with environment-specific overrides
baseConfig + (environmentOverrides[env] || {})
2. Kubernetes Manifest Template
// templates/kubernetes.jsonnet
local appName = std.extVar("appName");
local environment = std.extVar("environment");
local replicas = std.extVar("replicas");
local image = std.extVar("image");
local version = std.extVar("version");
local resources = std.extVar("resources");
local labels = {
"app": appName,
"version": version,
"environment": environment,
"managed-by": "jsonnet"
};
local annotations = {
"prometheus.io/scrape": "true",
"prometheus.io/port": "8080",
"prometheus.io/path": "/actuator/prometheus"
};
{
deployment: {
apiVersion: "apps/v1",
kind: "Deployment",
metadata: {
name: appName,
namespace: environment,
labels: labels
},
spec: {
replicas: replicas,
selector: {
matchLabels: {
app: appName
}
},
template: {
metadata: {
labels: labels,
annotations: annotations
},
spec: {
containers: [
{
name: appName,
image: image + ":" + version,
ports: [
{
containerPort: 8080,
name: "http"
}
],
env: [
{
name: "SPRING_PROFILES_ACTIVE",
value: environment
},
{
name: "JAVA_OPTS",
value: "-Xmx" + resources.memoryLimit + " -Xms" + resources.memoryRequest
}
],
resources: {
requests: {
memory: resources.memoryRequest,
cpu: resources.cpuRequest
},
limits: {
memory: resources.memoryLimit,
cpu: resources.cpuLimit
}
},
livenessProbe: {
httpGet: {
path: "/actuator/health",
port: 8080
},
initialDelaySeconds: 30,
periodSeconds: 10
},
readinessProbe: {
httpGet: {
path: "/actuator/health",
port: 8080
},
initialDelaySeconds: 5,
periodSeconds: 5
}
}
]
}
}
}
},
service: {
apiVersion: "v1",
kind: "Service",
metadata: {
name: appName,
namespace: environment,
labels: labels
},
spec: {
selector: {
app: appName
},
ports: [
{
port: 80,
targetPort: 8080,
protocol: "TCP"
}
],
type: if environment == "production" then "LoadBalancer" else "ClusterIP"
}
},
configMap: {
apiVersion: "v1",
kind: "ConfigMap",
metadata: {
name: appName + "-config",
namespace: environment
},
data: {
"application.properties": |||
spring.application.name=%(appName)s
server.port=8080
management.endpoints.web.exposure.include=health,info,metrics,prometheus
logging.level.com.example=INFO
||| % { appName: appName }
}
}
}
3. Multi-Environment Configuration Template
// templates/multi-env.jsonnet
local environments = std.extVar("environments");
local applications = std.extVar("applications");
local defaults = std.extVar("defaults");
// Common configuration for all environments
local commonConfig = {
server: {
port: 8080,
contextPath: "/api"
},
database: {
driverClassName: "org.postgresql.Driver",
minIdle: 2,
connectionTimeout: 30000
},
redis: {
port: 6379,
database: 0,
timeout: 2000
},
security: {
jwtExpiration: 86400000
}
};
// Environment-specific configurations
local environmentConfigs = {
development: {
server: {
port: 8080
},
database: {
url: "jdbc:postgresql://localhost:5432/devdb",
username: "dev_user",
maxPoolSize: 5,
showSql: true
},
redis: {
host: "localhost",
cluster: false
},
security: {
enabled: false
},
logging: {
level: "DEBUG",
file: "application.log"
}
},
staging: {
database: {
url: "jdbc:postgresql://staging-db:5432/stagingdb",
username: "staging_user",
maxPoolSize: 10,
showSql: false
},
redis: {
host: "staging-redis",
cluster: false
},
security: {
enabled: true
},
logging: {
level: "INFO",
file: "/var/log/application.log"
}
},
production: {
database: {
url: "jdbc:postgresql://prod-db:5432/proddb",
username: "prod_user",
maxPoolSize: 20,
showSql: false
},
redis: {
host: "redis-cluster",
cluster: true
},
security: {
enabled: true
},
logging: {
level: "WARN",
file: "/var/log/application.log"
}
}
};
// Function to create application config
local createAppConfig(env, app) = (
local baseConfig = commonConfig + environmentConfigs[env];
baseConfig + {
name: app,
environment: env,
version: defaults.version
}
);
// Generate configuration for all environments and applications
{
environments: std.objectFields(environmentConfigs),
applications: applications,
configs: std.objectMap(
function(env) std.objectMap(
function(app) createAppConfig(env, app),
applications
),
std.objectFields(environmentConfigs)
),
shared: {
database: {
driverClassName: commonConfig.database.driverClassName
},
security: {
jwtExpiration: commonConfig.security.jwtExpiration
}
}
}
4. Library/Import Template
// lib/database.libsonnet
{
// Database configuration factory
new(name, env, overrides={})::
local baseConfig = {
development: {
url: "jdbc:postgresql://localhost:5432/" + name + "db",
username: "dev_user",
maxPoolSize: 5,
showSql: true
},
staging: {
url: "jdbc:postgresql://staging-db:5432/" + name + "db",
username: "staging_user",
maxPoolSize: 10,
showSql: false
},
production: {
url: "jdbc:postgresql://prod-db:5432/" + name + "db",
username: "prod_user",
maxPoolSize: 20,
showSql: false
}
};
baseConfig[env] + overrides,
// Connection pool configuration
connectionPool(minIdle=2, maxPoolSize=10, timeout=30000): {
minIdle: minIdle,
maxPoolSize: maxPoolSize,
connectionTimeout: timeout,
validationQuery: "SELECT 1",
testOnBorrow: true
}
}
// lib/security.libsonnet
{
// Security configuration factory
new(env, overrides={})::
local baseConfig = {
development: {
enabled: false,
corsOrigins: ["http://localhost:3000", "http://localhost:8080"]
},
staging: {
enabled: true,
corsOrigins: ["https://staging.company.com"]
},
production: {
enabled: true,
corsOrigins: ["https://company.com", "https://www.company.com"]
}
};
baseConfig[env] + overrides,
// JWT configuration
jwt(secret, expiration=86400000): {
jwtSecret: secret,
jwtExpiration: expiration
}
}
// templates/with-imports.jsonnet
local database = import "lib/database.libsonnet";
local security = import "lib/security.libsonnet";
local env = std.extVar("env");
local appName = std.extVar("appName");
{
name: appName,
environment: env,
database: database.new(appName, env) + database.connectionPool(2, 10, 30000),
security: security.new(env) + security.jwt(std.extVar("jwtSecret")),
server: {
port: 8080,
contextPath: "/api"
},
logging: {
level: if env == "production" then "INFO" else "DEBUG"
}
}

Spring Boot Integration

1. Jsonnet Configuration Properties
@ConfigurationProperties(prefix = "jsonnet")
@Data
public class JsonnetProperties {
private boolean enabled = true;
private String templateDirectory = "templates";
private List<String> importPaths = List.of("lib");
private Map<String, String> defaultVariables = new HashMap<>();
private Cache cache = new Cache();
@Data
public static class Cache {
private boolean enabled = true;
private Duration ttl = Duration.ofMinutes(30);
private int maxSize = 1000;
}
}
2. Jsonnet Configuration Loader
@Service
@Slf4j
public class JsonnetConfigurationLoader {
private final JsonnetTemplateService templateService;
private final JsonnetProperties properties;
private final CacheManager cacheManager;
public JsonnetConfigurationLoader(JsonnetTemplateService templateService,
JsonnetProperties properties,
CacheManager cacheManager) {
this.templateService = templateService;
this.properties = properties;
this.cacheManager = cacheManager;
}
@PostConstruct
public void initialize() {
log.info("Jsonnet configuration loader initialized with template directory: {}", 
properties.getTemplateDirectory());
}
@Cacheable(value = "jsonnetConfigs", key = "#environment + '-' + #profile.name")
public ApplicationConfig loadApplicationConfig(String environment, ApplicationProfile profile) {
log.info("Loading application configuration for {}:{}", environment, profile.getName());
try {
return templateService.generateApplicationConfig(environment, profile);
} catch (Exception e) {
log.error("Failed to load application configuration: {}", e.getMessage());
throw new ConfigurationLoadingException("Failed to load configuration", e);
}
}
public Map<String, Object> loadConfigurationAsMap(String templateName, 
Map<String, Object> parameters) {
try {
String jsonResult = templateService.renderTemplate(templateName, parameters);
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(jsonResult, Map.class);
} catch (Exception e) {
log.error("Failed to load configuration as map: {}", e.getMessage());
throw new ConfigurationLoadingException("Failed to load configuration map", e);
}
}
public void refreshCache() {
Cache cache = cacheManager.getCache("jsonnetConfigs");
if (cache != null) {
cache.clear();
log.info("Jsonnet configuration cache cleared");
}
}
public List<String> getAvailableTemplates() {
try {
Path templateDir = Paths.get(properties.getTemplateDirectory());
if (!Files.exists(templateDir)) {
return List.of();
}
return Files.walk(templateDir)
.filter(path -> path.toString().endsWith(".jsonnet"))
.map(path -> templateDir.relativize(path).toString())
.map(name -> name.replace(".jsonnet", ""))
.collect(Collectors.toList());
} catch (IOException e) {
log.warn("Failed to list available templates: {}", e.getMessage());
return List.of();
}
}
}
public class ConfigurationLoadingException extends RuntimeException {
public ConfigurationLoadingException(String message) {
super(message);
}
public ConfigurationLoadingException(String message, Throwable cause) {
super(message, cause);
}
}
3. Spring Configuration
@Configuration
@EnableConfigurationProperties(JsonnetProperties.class)
@EnableCaching
@Slf4j
public class JsonnetConfiguration {
@Bean
@ConditionalOnMissingBean
public JsonnetEvaluationService jsonnetEvaluationService() {
return new JsonnetEvaluationService();
}
@Bean
@ConditionalOnMissingBean
public JsonnetTemplateService jsonnetTemplateService(JsonnetEvaluationService evaluationService) {
return new JsonnetTemplateService(evaluationService);
}
@Bean
@ConditionalOnMissingBean
public JsonnetConfigurationLoader jsonnetConfigurationLoader(
JsonnetTemplateService templateService,
JsonnetProperties properties,
CacheManager cacheManager) {
return new JsonnetConfigurationLoader(templateService, properties, cacheManager);
}
@Bean
public CacheManager cacheManager(JsonnetProperties properties) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
if (properties.getCache().isEnabled()) {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.expireAfterWrite(properties.getCache().getTtl())
.maximumSize(properties.getCache().getMaxSize());
cacheManager.setCaffeine(caffeine);
}
return cacheManager;
}
}

REST API Controllers

1. Configuration Management API
@RestController
@RequestMapping("/api/config")
@Slf4j
public class ConfigurationController {
private final JsonnetConfigurationLoader configLoader;
private final JsonnetEvaluationService evaluationService;
private final JsonnetTemplateService templateService;
public ConfigurationController(JsonnetConfigurationLoader configLoader,
JsonnetEvaluationService evaluationService,
JsonnetTemplateService templateService) {
this.configLoader = configLoader;
this.evaluationService = evaluationService;
this.templateService = templateService;
}
@GetMapping("/application/{environment}")
public ResponseEntity<ApplicationConfig> getApplicationConfig(
@PathVariable String environment,
@RequestParam(defaultValue = "default") String profile) {
log.info("Loading application configuration for {}:{}", environment, profile);
try {
ApplicationProfile appProfile = ApplicationProfile.builder()
.name(profile)
.featureFlags(Map.of())
.secrets(Map.of())
.corsOrigins(List.of())
.build();
ApplicationConfig config = configLoader.loadApplicationConfig(environment, appProfile);
return ResponseEntity.ok(config);
} catch (Exception e) {
log.error("Failed to load application configuration: {}", e.getMessage());
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/evaluate")
public ResponseEntity<EvaluationResult> evaluateJsonnet(@RequestBody EvaluationRequest request) {
log.info("Evaluating Jsonnet code with {} external variables", 
request.getExternalVariables().size());
try {
JsonNode result = evaluationService.evaluateJsonnet(
request.getJsonnetCode(), 
request.getExternalVariables()
);
EvaluationResult evaluationResult = EvaluationResult.builder()
.success(true)
.result(result)
.build();
return ResponseEntity.ok(evaluationResult);
} catch (JsonnetEvaluationException e) {
EvaluationResult errorResult = EvaluationResult.builder()
.success(false)
.error(e.getMessage())
.build();
return ResponseEntity.badRequest().body(errorResult);
}
}
@PostMapping("/template/{templateName}")
public ResponseEntity<TemplateResult> renderTemplate(
@PathVariable String templateName,
@RequestBody Map<String, Object> parameters) {
log.info("Rendering template {} with {} parameters", templateName, parameters.size());
try {
String result = templateService.renderTemplate(templateName, parameters);
TemplateResult templateResult = TemplateResult.builder()
.templateName(templateName)
.renderedContent(result)
.success(true)
.build();
return ResponseEntity.ok(templateResult);
} catch (Exception e) {
log.error("Failed to render template {}: {}", templateName, e.getMessage());
TemplateResult errorResult = TemplateResult.builder()
.templateName(templateName)
.success(false)
.error(e.getMessage())
.build();
return ResponseEntity.badRequest().body(errorResult);
}
}
@PostMapping("/validate")
public ResponseEntity<ValidationResult> validateJsonnet(@RequestBody ValidationRequest request) {
ValidationResult result = evaluationService.validateJsonnet(request.getJsonnetCode());
return ResponseEntity.ok(result);
}
@PostMapping("/cache/refresh")
public ResponseEntity<String> refreshCache() {
configLoader.refreshCache();
return ResponseEntity.ok("Configuration cache refreshed successfully");
}
@GetMapping("/templates")
public ResponseEntity<List<String>> getAvailableTemplates() {
List<String> templates = configLoader.getAvailableTemplates();
return ResponseEntity.ok(templates);
}
@PostMapping("/kubernetes/{appName}")
public ResponseEntity<KubernetesManifest> generateKubernetesManifest(
@PathVariable String appName,
@RequestBody DeploymentSpec spec) {
try {
KubernetesManifest manifest = templateService.generateKubernetesManifest(
appName, spec.getEnvironment(), spec);
return ResponseEntity.ok(manifest);
} catch (Exception e) {
log.error("Failed to generate Kubernetes manifest: {}", e.getMessage());
return ResponseEntity.internalServerError().build();
}
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class EvaluationRequest {
private String jsonnetCode;
private Map<String, Object> externalVariables;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class EvaluationResult {
private boolean success;
private JsonNode result;
private String error;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class TemplateResult {
private String templateName;
private String renderedContent;
private boolean success;
private String error;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class ValidationRequest {
private String jsonnetCode;
}

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class)
class JsonnetEvaluationServiceTest {
@InjectMocks
private JsonnetEvaluationService evaluationService;
@Test
void shouldEvaluateSimpleJsonnet() {
// Given
String jsonnetCode = "{ app: 'test', version: '1.0.0' }";
Map<String, Object> extVars = Map.of();
// When
JsonNode result = evaluationService.evaluateJsonnet(jsonnetCode, extVars);
// Then
assertThat(result.get("app").asText()).isEqualTo("test");
assertThat(result.get("version").asText()).isEqualTo("1.0.0");
}
@Test
void shouldEvaluateWithExternalVariables() {
// Given
String jsonnetCode = "{ app: 'test', env: std.extVar('environment') }";
Map<String, Object> extVars = Map.of("environment", "production");
// When
JsonNode result = evaluationService.evaluateJsonnet(jsonnetCode, extVars);
// Then
assertThat(result.get("app").asText()).isEqualTo("test");
assertThat(result.get("env").asText()).isEqualTo("production");
}
@Test
void shouldHandleComplexObjects() {
// Given
String jsonnetCode = """
{
app: 'test',
database: {
url: std.extVar('dbUrl'),
poolSize: std.extVar('poolSize')
}
}
""";
Map<String, Object> extVars = Map.of(
"dbUrl", "jdbc:postgresql://localhost:5432/test",
"poolSize", "10"
);
// When
JsonNode result = evaluationService.evaluateJsonnet(jsonnetCode, extVars);
// Then
assertThat(result.get("app").asText()).isEqualTo("test");
assertThat(result.get("database").get("url").asText()).isEqualTo("jdbc:postgresql://localhost:5432/test");
assertThat(result.get("database").get("poolSize").asText()).isEqualTo("10");
}
}
@SpringBootTest
class JsonnetTemplateServiceIntegrationTest {
@Autowired
private JsonnetTemplateService templateService;
@Test
void shouldGenerateApplicationConfig() {
// Given
String environment = "development";
ApplicationProfile profile = ApplicationProfile.builder()
.name("default")
.featureFlags(Map.of("newFeature", true))
.secrets(Map.of())
.corsOrigins(List.of("http://localhost:3000"))
.build();
// When
ApplicationConfig config = templateService.generateApplicationConfig(environment, profile);
// Then
assertThat(config).isNotNull();
assertThat(config.getEnvironment()).isEqualTo(environment);
assertThat(config.getDatabase()).isNotNull();
assertThat(config.getSecurity()).isNotNull();
}
}
2. Test Configuration
@TestConfiguration
public class TestJsonnetConfiguration {
@Bean
public JsonnetEvaluationService jsonnetEvaluationService() {
return new JsonnetEvaluationService();
}
@Bean
public JsonnetTemplateService jsonnetTemplateService(JsonnetEvaluationService evaluationService) {
return new JsonnetTemplateService(evaluationService);
}
}

Best Practices

1. Configuration Structure
// Best practice: Use libraries for reusable components
local lib = import "lib/commons.libsonnet";
{
// Use functions for complex logic
createAppConfig(name, env, overrides={})::
local base = lib.baseConfig(env);
base + {
name: name,
environment: env
} + overrides,
// Use variables for constants
defaultPort: 8080,
defaultDbPool: 10,
// Use conditionals for environment-specific values
getDatabaseUrl(env)::
if env == "production" then
"jdbc:postgresql://prod-db:5432/appdb"
else if env == "staging" then
"jdbc:postgresql://staging-db:5432/appdb"
else
"jdbc:postgresql://localhost:5432/appdb"
}
2. Error Handling and Validation
@Service
@Slf4j
public class JsonnetValidationService {
private final JsonnetEvaluationService evaluationService;
public JsonnetValidationService(JsonnetEvaluationService evaluationService) {
this.evaluationService = evaluationService;
}
public TemplateValidationResult validateTemplate(String templateName, String jsonnetCode) {
List<String> errors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
// Check for common issues
if (!jsonnetCode.contains("std.extVar")) {
warnings.add("Template does not use external variables - consider making it more dynamic");
}
if (jsonnetCode.contains("localhost") && !jsonnetCode.contains("if")) {
warnings.add("Template contains hardcoded 'localhost' - consider using environment variables");
}
// Validate syntax
try {
evaluationService.validateJsonnet(jsonnetCode);
} catch (Exception e) {
errors.add("Syntax error: " + e.getMessage());
}
// Check for required external variables
List<String> requiredVars = extractRequiredVariables(jsonnetCode);
if (!requiredVars.isEmpty()) {
warnings.add("Template requires external variables: " + String.join(", ", requiredVars));
}
return TemplateValidationResult.builder()
.templateName(templateName)
.valid(errors.isEmpty())
.errors(errors)
.warnings(warnings)
.requiredVariables(requiredVars)
.build();
}
private List<String> extractRequiredVariables(String jsonnetCode) {
List<String> variables = new ArrayList<>();
// Simple regex to find std.extVar usage
Pattern pattern = Pattern.compile("std\\.extVar\\('([^']+)'\\)");
Matcher matcher = pattern.matcher(jsonnetCode);
while (matcher.find()) {
variables.add(matcher.group(1));
}
return variables.stream().distinct().collect(Collectors.toList());
}
}

Conclusion

Implementing Jsonnet for configuration in Java provides:

  • Dynamic configuration generation with variables and functions
  • Environment-specific configurations without code duplication
  • Modular configuration composition through imports
  • Type-safe configuration objects in Java
  • Validation and testing capabilities for configurations

Key benefits:

  1. Reduced configuration duplication through templating
  2. Environment-aware configurations with conditionals
  3. Reusable configuration components via libraries
  4. Integration with existing Java ecosystems (Spring Boot, etc.)
  5. Validation and error checking before deployment
  6. Multi-format output (JSON, YAML, etc.)

This approach is particularly valuable for:

  • Microservices architectures with multiple environments
  • Kubernetes deployments requiring complex manifests
  • Applications with feature flags and dynamic configuration
  • Teams needing consistent configurations across environments

By leveraging Jsonnet's powerful templating capabilities combined with Java's type safety and ecosystem integration, you can create maintainable, scalable, and dynamic configuration management systems.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper