Declarative YAML format
OpenRewrite allows you to create recipes and styles in YAML. While doing so potentially reduces customizability, it makes up for that with development speed and portability.
To help you confidently define recipes and styles in YAML, this guide will walk you through all of the ways you can configure an OpenRewrite YAML file.
Note that values passed to declarative recipes are subject to YAML interpretation. So 1.20 would be interpreted as a float, causing the trailing 0 to be dropped. Wrapping the value in single or double quotes will resolve this.
Where OpenRewrite YAML files can exist
There are two places where you can define an OpenRewrite YAML file:
- Within the
rewrite.ymlfile of a project that applies rewrite recipes via the rewrite-gradle-plugin or rewrite-maven-plugin - Inside the
META-INF/rewritefolder of a JAR (such as in the rewrite-testing-frameworks)
If you define a recipe or style in the rewrite.yml file, they will not be included in the JARs published from your project.
If you want to distribute a recipe or a style and apply them to other projects, you'll need to create them inside of the META-INF/rewrite folder of a JAR.
Best practices
Please keep these conventions in mind when you're creating OpenRewrite YAML files:
- A file may contain any number of recipes and styles, separated by
---. - Within a file, recipe and style names must be fully qualified.
- Custom recipes should not be placed into the
org.openrewritenamespace. Instead, they should follow the same reverse domain name notation used in Java packages.
Recipes
Format
You can find the full schema for every resource type in rewrite-core/openrewrite.json.
| Key | Type | Description |
|---|---|---|
| type | const | A constant: specs.openrewrite.org/v1beta/recipe |
| name | string | A fully qualified, unique name for this recipe (required) |
| displayName | string | A human-readable name for this recipe (does not end with a period) |
| description | string | A human-readable description for this recipe (ends with a period) |
| tags | array of strings | A list of strings that help categorize this recipe |
| estimatedEffortPerOccurrence | duration | The expected amount of time saved each time this recipe fixes something |
| causesAnotherCycle | boolean | Whether or not this recipe can cause another cycle (defaults to false) |
| preconditions | array of recipes | Recipes that gate which source files this recipe may edit |
| recipeList | array of recipes | The list of recipes which comprise this recipe |
Preconditions
Preconditions are used to limit which source files a recipe is allowed to make edits to. In other words, they act as filters that allow you to target specific files, directories, or patterns.
This is particularly useful when you want a recipe to run only on a subset of the codebase.
Technically, almost any recipe can serve as a precondition, but in practice, lightweight and fast recipes – often based on simple searches – are preferred. These ensure performance remains optimal during large-scale code transformations.
When a recipe is used as a precondition, it determines whether a source file should be considered eligible for transformation. In other words, preconditions don’t make changes themselves, they just decide if the targeted recipe(s) should be allowed to make changes to a file.
If a file does not satisfy the precondition, the recipe list is skipped for that file entirely. When multiple recipes are used as preconditions, all of them must make a change to the file for it to be considered to meet the precondition.
Changes made by preconditions are not included in the final result of the recipe. Changes made by preconditions are used only to determine if the recipe should edit a particular source file.
It's important to understand that preconditions operate on already-parsed source files. OpenRewrite runs in two distinct phases:
- Parsing phase: All source files (except those in
exclusions) are parsed into LSTs - Recipe execution phase: Preconditions determine which parsed files the recipe should modify
This means preconditions cannot prevent files from being parsed - they only control whether recipes apply to files that have already been successfully parsed.
Preconditions only apply to files that already exist in the source set. They cannot prevent the creation of new files.
If a recipe generates files during the generate phase, those files will always be created because preconditions cannot evaluate files that aren't part of the current source set.
To conditionally generate files, implement a custom scanning recipe. You can define logic in the scanning phase based on existing source files, and use that context in the generate phase to control whether a file should be created.
Preconditions vs. Exclusions
Since preconditions work on already-parsed files, they're not the right tool if you need to skip parsing certain files. Below is a table that should help you make an informed decision on when to use one feature over another:
| Feature | Purpose | When It Runs | Use Case |
|---|---|---|---|
| Exclusions | Skip parsing files entirely | During parsing phase | Avoid parse errors, skip expensive directories, ignore generated code |
| Preconditions | Control recipe application | After parsing phase | Apply recipes to specific subsets of successfully-parsed files |
Example scenario: If you have Groovy files that fail to parse in a Java project:
- ❌ Don't use preconditions - the Groovy files will still be parsed and fail
- ✅ Use exclusions - skip parsing Groovy files:
exclusion("**/*.groovy")
For information on how to configure each of these, please see:
Adding preconditions to a YAML recipe
To create these top-level preconditions, you'll need to add the preconditions map to your declarative recipe's YAML. This object is a list of one or more recipes (formatted the same way as the recipeList).
---
type: specs.openrewrite.org/v1beta/recipe
name: org.openrewrite.PreconditionExample
preconditions:
- org.openrewrite.text.Find:
find: 1
recipeList:
- org.openrewrite.text.ChangeText:
toText: 2
On its own ChangeText would change the contents of all text files in the project to 2.
But because Find is used as a precondition, ChangeText will only be run on files that contain a 1.
Recipes commonly used as preconditions include:
org.openrewrite.FindSourceFiles- limits the recipe to only run on files whose path matches a glob patternorg.openrewrite.text.Find- limits the recipe to only run on files that contain a given stringorg.openrewrite.java.search.FindTypes- limits the recipe to run only on source code which contain a given typeorg.openrewrite.java.search.HasJavaVersion- limits the recipe to run only on Java source code with the specified source or target compatibility versions. Allowing a recipe to be targeted only at Java 8, 11, 17, etc., code.org.openrewrite.java.search.IsLikelyTest- limits the recipe to run only on source code which is likely to be test code.org.openrewrite.java.search.IsLikelyNotTest- limits the recipe to run only on source code which is likely to be production code.org.openrewrite.java.dependencies.search.ModuleHasDependency- limits the recipe to run on all files within a module that has a matching dependency on its classpath.org.openrewrite.java.dependencies.search.DoesNotIncludeDependency- limits the recipe to run on all files within a module that does not have a matching dependency on its classpath.org.openrewrite.gradle.search.ModuleHasPlugin- limits the recipe to run on all files within a Gradle module that applies a matching plugin.
Recipes like ModuleHasDependency and ModuleHasPlugin mark every file within a module when the dependency or plugin is found – not just the build.gradle, pom.xml, or other file that declares it. This is what makes them suitable as preconditions for recipes that need to edit source files (such as Java classes) based on module-level characteristics. See Precondition scope below for more on why this matters.
Creating "OR" preconditions instead of "AND"
As mentioned above, every recipe in the preconditions section is run and must apply to a file for it to "meet the precondition". However, what if you wanted to write a recipe where you had many precondition recipes – but you wanted to check if any of them pass rather than all of them?
To do this, you'll want to create a recipe that wraps all of your preconditions up into one recipe, and then use that recipe as the precondition such as in the following example:
type: specs.openrewrite.org/v1beta/recipe
name: org.sample.DoSomething
displayName: Do Something
preconditions:
- org.sample.FindAnyJson
recipeList:
- org.openrewrite.json.ChangeKey:
oldKeyPath: $.foo
newKey: bar
---
type: specs.openrewrite.org/v1beta/recipe
name: org.sample.FindAnyJson
recipeList:
- org.openrewrite.FindSourceFiles:
filePattern: "**/my.json"
- org.openrewrite.FindSourceFiles:
filePattern: "**/your.json"
- org.openrewrite.FindSourceFiles:
filePattern: "**/our.json"
In this example, if a file matches **/my.json OR **/your.json* OR **/our.json, then the precondition has passed and the ChangeKey recipe will be applied to it.
Precondition scope
A common mistake new users make is to assume that if a precondition matches any file, then the recipe will apply to the entire repository.
For example, you might be tempted to use FindPlugins as a precondition for RemoveUnusedImports with the idea that you only want unused imports removed on Gradle projects that apply a specific plugin.
However, if you were to do this, you wouldn't get the results you expected as the FindPlugins recipe flags the plugin in build.gradle files. In other words, the RemoveUnusedImports recipe would only be run against those build.gradle files.
Fortunately, there are recipes that can be used in this type of situation. For instance, the ModuleHasPlugin recipe will mark all files within a project if a specific plugin is found:
type: specs.openrewrite.org/v1beta/recipe
name: org.sample.CleanUpSonarProjects
displayName: Remove unused imports in sonar projects
description: >-
This recipe removes unused imports only in gradle projects that apply the sonar plugin.
This works because ModuleHasPlugin will mark all files within a project that applies the plugin.
preconditions:
- org.openrewrite.gradle.search.ModuleHasPlugin:
pluginId: org.sonarqube
recipeList:
- org.openrewrite.java.RemoveUnusedImports
It isn't obvious from just the names of the recipes that FindPlugins and ModuleHasPlugin behave differently. Because of that, the best way for you to know whether a particular recipe is suitable as a precondition or not is to run that recipe on its own.
If the recipe adds search markers or edits to every individual source file you want changes made to, it is suitable as a precondition. If not, then you'll need to find another recipe.
Before OpenRewrite 8.52.0 ScanningRecipes were not supported as YAML preconditions.
If you encounter errors attempting to use recipes like ModuleHasPlugin as preconditions ensure you are using a recent version of OpenRewrite.
Using preconditions to limit what files are touched
You can use preconditions to limit what files are touched by using the FindSourceFiles recipe and passing in a negation to it. For instance, in the example below, we tell the recipe to run on every file except for the PackageSettings.java file:
type: specs.openrewrite.org/v1beta/recipe
name: org.openrewrite.PreconditionExample
preconditions:
- org.openrewrite.FindSourceFiles:
filePattern: "!(**/PackageSettings.java)"
recipeList:
- org.openrewrite.text.ChangeText:
toText: 2
Recipe list
A declarative recipe can be made up of one or more recipes. The recipes in the list could be other declarative recipes defined in the same file or they can be imperative recipes created elsewhere. Like imperative recipes, each recipe in this list can potentially have configuration options that need to be specified.
Recipes in the recipeList will run in the order they are listed. That being said, a declarative recipe may include another declarative recipe declared later in the same rewrite.yml file.
Recipe example
Consider this example declarative recipe:
---
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.RecipeA
displayName: Recipe A
description: Applies Recipe B.
tags:
- tag1
- tag2
estimatedEffortPerOccurrence: PT15M
causesAnotherCycle: true
recipeList:
- com.yourorg.RecipeB:
exampleConfig1: foo
exampleConfig2: bar
- com.yourorg.RecipeC
If you wanted to run this recipe (but not distribute it to others), you would:
- Copy the above YAML into a
rewrite.ymlfile at the root of your project - Configure the Gradle plugin or Maven plugin to have an active recipe of
com.yourorg.RecipeA - Run the
mvn rewrite:runor thegradle rewriteRuncommand
Styles
Format
You can find the full style schema in rewrite-core/openrewrite.json.
| Key | Type | Description |
|---|---|---|
| type | const | A constant: specs.openrewrite.org/v1beta/style |
| name | string | A fully qualified, unique name for this style (required) |
| displayName | string | A human-readable name for this style (does not end with a period) |
| description | string | A human-readable description for this style (ends with a period) |
| tags | array of strings | A list of strings that help categorize this style |
| styleConfigs | array of styles | The list of styles which comprise this style |
Style example
Consider this example declarative style, which specifies that tabs should be used for indentation and that at least 9999 imports from a given package should be required before collapsing them into a single star import:
---
type: specs.openrewrite.org/v1beta/style
name: com.yourorg.YesTabsNoStarImports
styleConfigs:
- org.openrewrite.java.style.TabsAndIndentsStyle:
useTabCharacter: true
- org.openrewrite.java.style.ImportLayoutStyle:
classCountToUseStarImport: 9999
To put this style in effect for any formatting performed by OpenRewrite within the current project:
- Put the above into a
rewrite.ymlfile at the project root - Configure the gradle plugin or maven plugin with
com.yourorg.YesTabsNoStarImportslisted as the active style
The next time any OpenRewrite recipe is run in that project, any formatting it performs will take these styles into account.
Categories
Categories control how recipes are grouped, named, and described in the recipe catalog. Every OpenRewrite language module ships one (for example rewrite-yaml's yaml-categories.yml), and you can define your own so that your recipes are presented sensibly alongside them.
Format
| Key | Type | Description |
|---|---|---|
| type | const | A constant: specs.openrewrite.org/v1beta/category |
| packageName | string | The package this category describes, e.g. org.openrewrite.yaml (required) |
| name | string | A human-readable name for the category |
| description | string | A human-readable description for the category (ends with a period) |
| tags | array of strings | A list of strings that help categorize this category |
| root | boolean | Whether this package is only a naming prefix; see root categories (defaults to false) |
| priority | integer | Sort order relative to sibling categories; lower sorts first (defaults to 0) |
Category example
---
type: specs.openrewrite.org/v1beta/category
name: Java
packageName: com.yourorg.java
description: Recipes for your organization's Java code.
priority: 1
How recipes are placed in categories
A recipe's position in the catalog comes from its name: every package segment ahead of the final class name becomes one level of nesting. com.yourorg.java.MigrateToSpringBoot3 sits under com > yourorg > java.
For each of those levels OpenRewrite looks for a category whose packageName matches the package up to and including that segment. Where one exists, its name, description, and priority are used; where none exists, a category is synthesized from the segment with its first letter capitalized. That is why an undeclared package shows up in the catalog as Yourorg rather than something readable.
Matching is against the whole partial package, never a bare segment: a category for com.yourorg describes com.yourorg only, and does not apply to a yourorg segment appearing anywhere else.
Root categories
root: true marks a package as a naming prefix rather than a category. A root category is not displayed at all; its subcategories are lifted into its parent, which for a top-level prefix means the top of the catalog. This is what keeps org.openrewrite.java recipes under Java instead of burying them under Org > Openrewrite > Java.
rewrite-core ships root categories for the reverse DNS prefixes it knows about — com, org, io, ai, tech, and software. Newer versions also root common generic and country code top-level domains, such as uk, uk.co, de, and nl.
Two things are worth knowing if you publish under a prefix that is not rooted:
- Each level needs its own root category. For
uk.co.acme, rooting onlyukpromotes Co to the top level; you need roots for bothukanduk.co. - Declaring a root category that
rewrite-corealready provides does no harm, so when in doubt, declare it.
---
type: specs.openrewrite.org/v1beta/category
packageName: uk
root: true
---
type: specs.openrewrite.org/v1beta/category
packageName: uk.co
root: true
---
type: specs.openrewrite.org/v1beta/category
name: Acme
packageName: uk.co.acme
description: Recipes for Acme's codebases.
Recipes named uk.co.acme.* are now presented under a single top-level Acme category, rather than under Uk > Co > Acme.
packageName values are subject to YAML interpretation, so a segment such as no or on has to be quoted to keep it from being read as a boolean.
Examples
Examples attach before/after snippets to a recipe so that the generated recipe documentation can show what the recipe does. These are normally generated into a module's META-INF/rewrite/examples.yml from tests annotated with @DocumentExample, rather than written by hand — but it is useful to be able to read them.
Format
| Key | Type | Description |
|---|---|---|
| type | const | A constant: specs.openrewrite.org/v1beta/example |
| recipeName | string | The fully qualified name of the recipe these examples demonstrate |
| examples | array of examples | One or more examples for that recipe |
Each entry in examples accepts:
| Key | Type | Description |
|---|---|---|
| description | string | A human-readable description, usually the originating test's name |
| parameters | array of strings | The recipe options used, in declaration order, for recipes that take options |
| sources | array of sources | The source files the example operates on |
And each entry in sources accepts:
| Key | Type | Description |
|---|---|---|
| before | string | The source code before the recipe is run |
| after | string | The source code after the recipe is run; omit it if the file is unchanged |
| path | string | The source file's path, where the recipe depends on it |
| language | string | The source language, e.g. java, yaml, xml |
Example
---
type: specs.openrewrite.org/v1beta/example
recipeName: org.openrewrite.yaml.AddCommentToProperty
examples:
- description: '`AddCommentToPropertyTest#addCommentToNestedProperty`'
parameters:
- management.metrics.enabled
- This property is deprecated
- 'null'
- 'null'
sources:
- before: |
management:
metrics:
enabled: true
after: |
management:
metrics:
# This property is deprecated
enabled: true
language: yaml
Testing
For information on how to test declarative YAML recipes, check out our recipe testing guide