Skip to content

Normalize null collections in RecipeDescriptor deserialization#7545

Closed
jkschneider wants to merge 1 commit intomainfrom
recipe-descriptor-null-normalization
Closed

Normalize null collections in RecipeDescriptor deserialization#7545
jkschneider wants to merge 1 commit intomainfrom
recipe-descriptor-null-normalization

Conversation

@jkschneider
Copy link
Copy Markdown
Member

What

Recipe.createRecipeDescriptor() always passes non-null lists for tags, options, preconditions, recipeList, dataTables, maintainers, contributors, and examples — and callers across the ecosystem rely on that, iterating these getters without null checks.

When a RecipeDescriptor is deserialized from JSON returned by a polyglot RPC peer (rewrite-javascript-remote, rewrite-csharp-remote, rewrite-python-remote), those peers omit empty collection-valued fields. Jackson's default @AllArgsConstructor(@JsonCreator) binding then leaves those fields null, and downstream code blows up:

Cannot invoke "java.util.List.iterator()" because the return value of
"org.openrewrite.config.RecipeDescriptor.getPreconditions()" is null

How

Replace the Lombok @Value + @AllArgsConstructor(onConstructor = @__(@JsonCreator)) with an explicit @JsonCreator constructor that normalizes null → empty at the deserialization boundary. Field-level immutability, getters, equals/hashCode, toString, and @With are preserved via explicit private final + @Getter / @ToString / @EqualsAndHashCode.

Locally constructed RecipeDescriptors are unaffected (they were already passing non-null collections).

Test plan

  • New RecipeDescriptorTest.deserializeOmittedCollectionsAsEmpty locks the contract: a JSON document missing tags, options, preconditions, recipeList, dataTables, maintainers, contributors, and examples deserializes to a descriptor whose getters return empty collections, never null.
  • Full :rewrite-core:test passes.

`Recipe.createRecipeDescriptor()` always passes non-null lists for
`tags`, `options`, `preconditions`, `recipeList`, `dataTables`,
`maintainers`, `contributors`, and `examples` — so callers in the
ecosystem treat these getters as never-null and iterate them directly.

When a `RecipeDescriptor` arrives from a polyglot RPC peer
(rewrite-javascript-remote, rewrite-csharp-remote, rewrite-python-remote),
those peers omit empty collection-valued fields from the JSON they
return. Jackson's default `@AllArgsConstructor`-based deserialization
then leaves the corresponding fields `null`, breaking every downstream
caller that does `descriptor.getPreconditions().iterator()` without a
null check.

Replace the Lombok `@Value` + `@AllArgsConstructor(@JsonCreator)` setup
with an explicit `@JsonCreator` constructor that normalizes null to
empty collections at the deserialization boundary. Field semantics
(immutability, getters, equals/hashCode, `@With`) are preserved via
explicit `private final` fields plus `@Getter` / `@ToString` /
`@EqualsAndHashCode`.
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite May 1, 2026
@jkschneider jkschneider marked this pull request as draft May 1, 2026 20:40
@jkschneider
Copy link
Copy Markdown
Member Author

Withdrawing — taking a different approach. Will normalize at the per-language RPC bundle reader (NpmRecipeBundleReader / NuGetRecipeBundleReader / PipRecipeBundleReader) instead of restructuring RecipeDescriptor.

@jkschneider jkschneider closed this May 1, 2026
@jkschneider jkschneider deleted the recipe-descriptor-null-normalization branch May 1, 2026 20:43
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite May 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant