Skip to main content

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.mjml with 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):

ExceptionCause
MjmlValidationExceptionInput exceeds maxInputSize or maxNestingDepth
MjmlParseExceptionMalformed MJML (invalid XML structure)
MjmlIncludeExceptionmj-include path cannot be resolved
MjmlRenderExceptionUnexpected error during the render phase
MjmlExceptionGeneral 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).