Frequently Asked Questions
Why pure Java instead of wrapping the Node.js MJML library?
Existing MJML solutions for Java typically shell out to Node.js or embed a JavaScript runtime (GraalJS, Nashorn). This creates operational complexity:
- Node.js dependency: You need Node.js installed in your production environment, Docker image, or CI pipeline.
- Process overhead: Spawning a Node.js process per render adds latency and resource usage.
- JavaScript runtimes: Embedding GraalJS or Nashorn adds large transitive dependencies and startup time.
mjml-java is a pure Java implementation with zero external dependencies (only the JDK standard library at runtime). It works anywhere a JVM runs -- no Node.js, no native libraries, no JavaScript engine.
What MJML version is this compatible with?
mjml-java targets the MJML v4 specification. All 31 top-level renderable components are implemented:
- 8 head components
- 5 body layout components
- 7 content components
- 11 interactive components
MJML helper/control tags such as mj-all, mj-class, mj-selector, and mj-html-attribute are also supported where applicable.
Compatibility is verified by 40 golden file tests that compare mjml-java's output against HTML generated by the official MJML v4 Node.js toolchain.
Can I use this with Spring Boot?
Yes. Add the Maven dependency to your pom.xml and use MjmlRenderer directly:
@Service
public class EmailService {
public String renderTemplate(String mjml) {
return MjmlRenderer.render(mjml).html();
}
}
MjmlRenderer.render() is a static, thread-safe method. Each call creates its own internal pipeline, so concurrent calls do not share mutable state. The MjmlConfiguration object is immutable and safe to share across threads.
Can I use this with Quarkus or GraalVM native image?
Yes. mjml-java is designed for compatibility with ahead-of-time compilation:
- JPMS module: Declared as
dev.jcputney.mjmlwith explicit exports. - No reflection: The library does not use reflection, proxy generation, or dynamic class loading.
- Pure Java: Only JDK standard library APIs are used (XML parsing, string manipulation, collections).
These characteristics make it a good fit for GraalVM native image builds without additional configuration.
How do I handle dynamic templates?
mjml-java renders MJML strings to HTML. For dynamic content, apply your template engine before calling the renderer:
// Using any template engine (Thymeleaf, FreeMarker, Mustache, etc.)
String mjml = templateEngine.process("email-template", context);
String html = MjmlRenderer.render(mjml).html();
Or use simple string interpolation for basic cases:
String mjml = """
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text>Hello, %s!</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
""".formatted(userName);
String html = MjmlRenderer.render(mjml).html();
What about performance?
mjml-java is designed for server-side rendering. Each render() call creates a lightweight pipeline object, and core component registries are reused through a bounded shared cache for efficiency. The render path itself does not share per-request mutable state, so it scales naturally with concurrent requests.
Key characteristics:
- No warm-up needed: Unlike JavaScript runtimes, there is no JIT compilation of a JS engine on first call.
- Low allocation overhead: The pipeline uses string builders and small data structures.
- Predictable latency: No GC pauses from a large embedded runtime.
For high-throughput scenarios, the renderer works well behind a thread pool or reactive framework without additional pooling or caching.
Are there known differences from the official MJML output?
mjml-java aims for pixel-perfect compatibility with the official MJML v4 toolchain. The 40 golden file tests verify this by comparing rendered HTML character-by-character against reference output.
Minor differences may exist in edge cases related to CSS inlining, since the official MJML library uses the juice npm package while mjml-java uses its own CSS inlining engine. These differences are tracked and minimized through the golden file test suite.
What exceptions should I handle?
mjml-java throws unchecked exceptions (subclasses of MjmlException):
| Exception | Cause |
|---|---|
MjmlValidationException | Input exceeds maxInputSize or maxNestingDepth |
MjmlParseException | Malformed MJML (invalid XML structure) |
MjmlIncludeException | mj-include path cannot be resolved |
MjmlRenderException | Unexpected error during the render phase |
MjmlException | General rendering errors (base type) |
Example:
try {
String html = MjmlRenderer.render(mjml).html();
} catch (MjmlValidationException e) {
// Input too large or too deeply nested
} catch (MjmlParseException e) {
// Malformed MJML input
} catch (MjmlException e) {
// Other rendering errors
}
Does mjml-java support mj-include?
Yes. Configure an IncludeResolver to enable include support:
MjmlConfiguration config = MjmlConfiguration.builder()
.includeResolver(new FileSystemIncludeResolver(Path.of("/templates")))
.build();
MjmlRenderResult result = MjmlRenderer.render(mjml, config);
The FileSystemIncludeResolver resolves paths relative to a base directory and includes path traversal protection. You can implement the IncludeResolver interface for custom resolution strategies (classpath, database, HTTP, etc.).
Can I register custom components?
Yes. Implement a ComponentFactory and register it via MjmlConfiguration.Builder:
MjmlConfiguration config = MjmlConfiguration.builder()
.registerComponent("mj-custom", (node, globalCtx, renderCtx) -> {
return new MyCustomComponent(node, globalCtx, renderCtx);
})
.build();
Custom components extend BodyComponent (for HTML-rendering components) or HeadComponent (for metadata-processing components).