MiniJinja 3: what you must check before you upgrade

Accent CMS renders every page with MiniJinja. MiniJinja 3.0 came out on 8 October 2026. It is a breaking release. I upgraded Accent in one pull request, and I kept notes. This post is the story of that pull request. It tells you what broke, what did not break, and what you must check in your own project.

I write this post in Simplified Technical English. Short sentences. One idea for each sentence. An upgrade guide must be easy to read.

Step 1: Let the compiler speak first

I changed one line in Cargo.toml and compiled. The compiler reported 67 errors. All of them were easy. They came in four groups:

  • Value::from_serialize is gone. You now wrap the value: Value::from(minijinja::value::Serde(data)).
  • The serde feature is now optional. Add features = ["serde"] if you serialize your own types.
  • The loader feature is gone. The loader API is now always there. Remove the feature from your Cargo.toml.
  • The context! macro now takes ownership. If you use a value after the macro, you must clone it first.

The compiler is your cheapest reviewer. But it only finds the easy problems. The hard problems compile without a warning.

Step 2: Find the silent map change

This was the most dangerous change in my upgrade. It made no compile error. It made no runtime error. It only made empty pages.

On MiniJinja 2, Value::from_iter over key-value pairs built a map. On MiniJinja 3, the same call builds a list of tuples. Your templates then read attributes from a list. They get undefined. MiniJinja prints undefined as an empty string.

The fix is one word. Use Value::from_pairs:

// MiniJinja 2: this built a map.
// MiniJinja 3: this builds a list of tuples. Your data is gone.
let context = Value::from_iter(vars);

// MiniJinja 3: this builds a map. Use this.
let context = Value::from_pairs(vars);

Search your code for from_iter. Check every call site. In Accent, this one change had emptied all nested frontmatter. Eighteen tests caught it. If you have no tests for nested data, write one before you upgrade.

Step 3: Expect stricter templates

MiniJinja 3 follows Python’s Jinja2 more closely. Some templates that ran on version 2 now stop with an error. Some now give a different result. Check your templates for these patterns:

Your templateMiniJinja 2MiniJinja 3
default(value=x)Works by accidentAn error. Write default(x)
1 / 0infAn error
2.5|round3.02.0 (rounds half to even)
"don't stop"|titleDon'T StopDon't Stop
7 // -2-3-4

The default(value=...) pattern comes from Tera and Zola. Two of our own themes used it. On MiniJinja 3, 42 of 44 pages on that site failed. Fix this pattern today. It is also wrong on MiniJinja 2. It just fails quietly there.

Step 4: Check printed lists and maps

MiniJinja 3 prints values in Python style. If a template prints a list or a map directly, the output changes:

TemplateMiniJinja 2MiniJinja 3
{{ ["a", "b"] }}["a", "b"]['a', 'b']
{{ {"a": 1}|items|list }}[["a", 1]][('a', 1)]

Most templates never print a raw list. But debug output and JSON-like output do. Search your templates for {{ followed by a bare list or map variable.

Step 5: Check key order

On MiniJinja 2, Accent’s objects listed their fields in alphabetical order. On MiniJinja 3, they keep declaration order. This matters only when a template loops over the keys of an object. If your template does {% for key in page %}, test it on both versions.

In our pull request, this is still an open decision. One test fails on purpose until we decide: keep the alphabetical order, or accept the new order. A failing test is a better reminder than a comment.

Step 6: Watch the unstable machinery API

If you use minijinja::machinery, read this step. If not, skip it.

We parse formula expressions with the machinery API. Its documentation marks it unstable, and version 3 proves that. The AST has a new Expr::Tuple variant. Our walker matches every variant without a wildcard, so the compiler pointed at each match. That was the goal of writing it that way. If your match arms use a wildcard, a new variant passes through them in silence. Remove the wildcard before you upgrade.

Step 7: Re-check on every patch release

I first ported the pull request on 3.0.0. Then 3.0.1 through 3.0.5 came out in three weeks. Each patch refined the Jinja2 compatibility: min and max case folding, round with a method argument, underscore imports, sorting lists of attributes.

Accent keeps 254 contract tests. Each test pins the exact output of one engine feature. I re-ran them on 3.0.5. All 254 passed. That is how a patch release becomes a five-minute check instead of a new review. If you do not have contract tests, start with ten. Pin the filters your themes use most.

The checklist

Do these steps in this order:

  1. Compile. Fix the removed APIs. They are easy.
  2. Search for Value::from_iter. Replace pair iterators with Value::from_pairs.
  3. Search your templates for default(value=. Fix each one.
  4. Check templates that divide, round, or title-case.
  5. Check templates that print raw lists or maps.
  6. Check templates that loop over object keys.
  7. If you use machinery, remove wildcards from your AST matches.
  8. Re-run your checks on every patch release.

Where our upgrade stands

The pull request is still a draft. The code compiles, clippy is clean, and 5,634 of 5,635 library tests pass. The one failure is the key-order test, and it fails on purpose. Some decisions are still open: key order, keyword arguments for plugin filters, and debug mode in development.

We also measured the upgrade the hard way: byte-for-byte build diffs, pixel diffs of the admin UI, and A/B/A benchmarks. MiniJinja 3 rendered every benchmark case 7.5 to 24.7 percent faster. You can read the full method in Byte for byte: how we test a template engine upgrade.

The engine decides what your pages say. Upgrade it with care.