Skip to main content

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.

Info

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:

  1. Within the rewrite.yml file of a project that applies rewrite recipes via the rewrite-gradle-plugin or rewrite-maven-plugin
  2. Inside the META-INF/rewrite folder 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.openrewrite namespace. Instead, they should follow the same reverse domain name notation used in Java packages.

Recipes​

Format​

Info

You can find the full schema for every resource type in rewrite-core/openrewrite.json.

KeyTypeDescription
typeconstA constant: specs.openrewrite.org/v1beta/recipe
namestringA fully qualified, unique name for this recipe (required)
displayNamestringA human-readable name for this recipe (does not end with a period)
descriptionstringA human-readable description for this recipe (ends with a period)
tagsarray of stringsA list of strings that help categorize this recipe
estimatedEffortPerOccurrencedurationThe expected amount of time saved each time this recipe fixes something
causesAnotherCyclebooleanWhether or not this recipe can cause another cycle (defaults to false)
preconditionsarray of recipesRecipes that gate which source files this recipe may edit
recipeListarray of recipesThe 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.

Info

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:

  1. Parsing phase: All source files (except those in exclusions) are parsed into LSTs
  2. 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.

Warning

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:

FeaturePurposeWhen It RunsUse Case
ExclusionsSkip parsing files entirelyDuring parsing phaseAvoid parse errors, skip expensive directories, ignore generated code
PreconditionsControl recipe applicationAfter parsing phaseApply 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:

Tip

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.

Warning

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.

Tip

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:

  1. Copy the above YAML into a rewrite.yml file at the root of your project
  2. Configure the Gradle plugin or Maven plugin to have an active recipe of com.yourorg.RecipeA
  3. Run the mvn rewrite:run or the gradle rewriteRun command

Styles​

Format​

Info

You can find the full style schema in rewrite-core/openrewrite.json.

KeyTypeDescription
typeconstA constant: specs.openrewrite.org/v1beta/style
namestringA fully qualified, unique name for this style (required)
displayNamestringA human-readable name for this style (does not end with a period)
descriptionstringA human-readable description for this style (ends with a period)
tagsarray of stringsA list of strings that help categorize this style
styleConfigsarray of stylesThe 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:

  1. Put the above into a rewrite.yml file at the project root
  2. Configure the gradle plugin or maven plugin with com.yourorg.YesTabsNoStarImports listed 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​

KeyTypeDescription
typeconstA constant: specs.openrewrite.org/v1beta/category
packageNamestringThe package this category describes, e.g. org.openrewrite.yaml (required)
namestringA human-readable name for the category
descriptionstringA human-readable description for the category (ends with a period)
tagsarray of stringsA list of strings that help categorize this category
rootbooleanWhether this package is only a naming prefix; see root categories (defaults to false)
priorityintegerSort 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 only uk promotes Co to the top level; you need roots for both uk and uk.co.
  • Declaring a root category that rewrite-core already 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.

Info

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​

KeyTypeDescription
typeconstA constant: specs.openrewrite.org/v1beta/example
recipeNamestringThe fully qualified name of the recipe these examples demonstrate
examplesarray of examplesOne or more examples for that recipe

Each entry in examples accepts:

KeyTypeDescription
descriptionstringA human-readable description, usually the originating test's name
parametersarray of stringsThe recipe options used, in declaration order, for recipes that take options
sourcesarray of sourcesThe source files the example operates on

And each entry in sources accepts:

KeyTypeDescription
beforestringThe source code before the recipe is run
afterstringThe source code after the recipe is run; omit it if the file is unchanged
pathstringThe source file's path, where the recipe depends on it
languagestringThe 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