Skip to content

Report unknown declarative recipe options as validation failures - #9393

Merged
timtebeek merged 2 commits into
mainfrom
tim/unknown-yaml-keys-ignored
Oct 27, 2026
Merged

timtebeek merged 2 commits into
mainfrom
tim/unknown-yaml-keys-ignored

Conversation

@timtebeek

@timtebeek timtebeek commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Option keys in declarative YAML that a recipe can't bind were silently dropped. Reflective construction only reads constructor parameters, and the Jackson fallback has FAIL_ON_UNKNOWN_PROPERTIES disabled. As a result, a typo or a stale option passed validateAll() and the recipe ran without it:

recipeList:
  - org.openrewrite.text.ChangeText:
      toTxt: Hello   # org.openrewrite.text.ChangeText.toTxt: is not an option of this recipe; valid options are: toText

#4377 originally made tests fail on unknown keys, through the FAIL_ON_UNKNOWN_PROPERTIES mapper customizer in RecipeSpec. #5050 then moved recipe construction onto RecipeLoader's own mapper, so that customizer no longer reached recipes, and RecipeLifecycleTest was flipped to canCall…WithUnnecessaryArgs. This PR restores the original intent for every caller, not just tests.

YamlResourceLoader now checks each key after the recipe is built. A key is known if it is a public constructor parameter name, an @Option field, or a property Jackson can set on the recipe class itself, including @JsonAlias aliases. @Option fields count even when Jackson can't set them, because Recipe.withOptions (the marketplace path) binds fields at any visibility. The check is skipped for:

  • RpcRecipe, whose options the remote validates;
  • recipes declaring @JsonIgnoreProperties(ignoreUnknown = true) or a @JsonAnySetter;
  • recipes whose class introspection fails, for example a missing optional dependency in a member signature, so a recipe that loaded fine is not reported invalid.

A DeclarativeRecipe reached through the marketplace takes no options. Keys are matched case-sensitively, like the reflective path, although Jackson accepts any case.

Merge after the downstream fixes

RewriteTest asserts validateAll(), and downstream CI picks up rewrite snapshots within minutes. These recipes pass unknown keys today, so their repos' tests break as soon as this merges:

In each, the recipe silently ignored the misspelled option. The github-actions, rewrite-spring, static-analysis and hibernate ones were actual behaviour bugs.

Verification

  • ./gradlew :rewrite-core:test :rewrite-test:test :rewrite-yaml:test passes, as do the inline-YAML recipe tests in rewrite-gradle, hcl, java-test, json, kotlin and maven. AppendToSequenceTest.appendTwice still passed fileMatcher, removed from AppendToSequence in Improved support for large repositories #3118, so I dropped that key.
  • I ran validateAll() over 842 declarative recipes: this repo's YAML plus 23 published recipe modules (spring, migrate-java, static-analysis, testing-frameworks, etc.). The only new failures were the four openrewrite ones above. With those fixes swapped into the jars, the failure count matches the unpatched baseline. Load time is unchanged within noise.
  • I ran the same check over the 1284 declarative recipes reachable from moderne-recipe-bom, covering every published io.moderne.recipe module plus the openrewrite modules it manages. The baseline had 0 failures. After this change, the only findings are the two moderneinc PRs above.

Option keys a recipe can't bind were silently dropped by both the
reflective and the Jackson construction paths, so typos and stale
options passed validateAll() while the recipe ran without them.
addRecipe.accept(recipeLoader.apply(recipeName, (Map<String, Object>) recipeArgs));
Recipe recipe = recipeLoader.apply(recipeName, options);
addRecipe.accept(recipe);
validateOptionNames(recipeName, recipe, options, addValidation);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems strong to fail to load if we had ignored this up to now; should we introduce a grace period where we only validate this in tests? Or would that risk too many untested recipes continuing to ignore invalid options?

@timtebeek
timtebeek marked this pull request as ready for review October 25, 2026 18:20
@timtebeek

Copy link
Copy Markdown
Member Author

As discussed on Slack: we've already cleared out the issues on openrewrite/; now doing the same for moderneinc/; those downstream PRs can land already; we'll hold off on this one until after the next release is out.

@timtebeek

Copy link
Copy Markdown
Member Author

With the 8.94.0 release now out we'll merge this one to avoid further potential issues in recipes. There's one more fix coming for rewrite-spring-to-quarkus.

@timtebeek
timtebeek merged commit be2b31d into main Oct 27, 2026
1 check passed
@timtebeek
timtebeek deleted the tim/unknown-yaml-keys-ignored branch October 27, 2026 10:10
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Oct 27, 2026
timtebeek pushed a commit to openrewrite/rewrite-spring-to-quarkus that referenced this pull request Oct 27, 2026
…nore (#97)

41 option keys in these declarative recipes are not options of the recipe they configure. The recipe ignores
them silently today, openrewrite/rewrite#9393 reports them as validation failures, and they shipped in v0.14.0.

32 are `onlyDirect: true` on `org.openrewrite.java.dependencies.DependencyInsight`, which has no such option and
so matched transitive dependencies too. A module that got `spring-boot-starter-web` only through a sibling module
was given its own `quarkus-resteasy-jackson`, and one that got `h2` that way its own `quarkus-jdbc-h2`.

`org.openrewrite.java.dependencies.FindDependency` matches only declared dependencies, in Maven and Gradle:

- The 28 `SpringBoot*`/`SpringKafka*ToQuarkus` preconditions in boot-starters.yml now use it.
- `H2TestDriverToQuarkus` now uses it and loses `scope: test`. Every declared scope is on Maven's test classpath,
  and Gradle never read that option.
- `H2DriverToQuarkus` and `DerbyDriverToQuarkus` only lose the key. Their `ChangeDependency` steps touch only
  declared dependencies anyway.
- `DerbyTestDriverToQuarkus` only loses the key. Its `derby|derbyclient` pattern matches nothing, because glob
  patterns have no alternation, so the recipe never runs either way.

`ModuleHasDependency` does take `onlyDirect`, but it records its matches before any recipe edits. With it,
`SpringBootWebToQuarkusClassic` would still match after `SpringBootWebToQuarkusReactive` removed the starter, and
both Quarkus REST stacks would be added. The AMQP and Kafka pairs work the same way.

The other 9 are `fileMatcher`, which neither `DeleteProperty` nor `DeleteKey` reads, so the DevTools and Actuator
deletions apply to every properties and YAML file. That stays. Only Spring Boot reads these keys, it also reads
them from files outside `application*` (`spring.config.import`, Spring Cloud's `bootstrap*`), and no other
property step here is limited to particular files. The two DevTools `DeleteKey` steps were then identical, so
they are merged.

New tests run `MigrateBootStarters` and `MigrateDatabaseDrivers` on two modules, the second getting the starter or
`h2` only through the first. Both failed before this change. Another test covers the merged `DeleteKey`. Against a
rewrite-core built from #9393, all 68 recipes here validate and no test fails on an unknown option. Before
this change, all 37 tests of the affected recipes failed against it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

1 participant