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_serializeis gone. You now wrap the value:Value::from(minijinja::value::Serde(data)).- The
serdefeature is now optional. Addfeatures = ["serde"]if you serialize your own types. - The
loaderfeature is gone. The loader API is now always there. Remove the feature from yourCargo.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 template | MiniJinja 2 | MiniJinja 3 |
|---|---|---|
default(value=x) | Works by accident | An error. Write default(x) |
1 / 0 | inf | An error |
2.5|round | 3.0 | 2.0 (rounds half to even) |
"don't stop"|title | Don'T Stop | Don'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:
| Template | MiniJinja 2 | MiniJinja 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:
- Compile. Fix the removed APIs. They are easy.
- Search for
Value::from_iter. Replace pair iterators withValue::from_pairs. - Search your templates for
default(value=. Fix each one. - Check templates that divide, round, or title-case.
- Check templates that print raw lists or maps.
- Check templates that loop over object keys.
- If you use
machinery, remove wildcards from your AST matches. - 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.