From 622b6a93b35f51fa9994c2ea2cf9349ea97510aa Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 12 Jan 2026 14:37:01 +0100 Subject: [PATCH 1/4] Convert `@ApiModel(reference)` to `@Schema(ref)` or `@Schema(implementation)` --- .../swagger/MigrateApiModelToSchema.java | 95 +++++++++++++++++++ .../swagger/MigrateApiModelToSchemaTest.java | 46 +++++++++ 2 files changed, 141 insertions(+) diff --git a/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java b/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java index 2e1c918..0b584ff 100644 --- a/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java +++ b/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java @@ -15,6 +15,7 @@ */ package org.openrewrite.openapi.swagger; +import org.jspecify.annotations.NonNull; import org.openrewrite.ExecutionContext; import org.openrewrite.Preconditions; import org.openrewrite.Recipe; @@ -24,6 +25,8 @@ import org.openrewrite.java.ChangeAnnotationAttributeName; import org.openrewrite.java.ChangeType; import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; import org.openrewrite.java.search.UsesType; import org.openrewrite.java.tree.Expression; import org.openrewrite.java.tree.J; @@ -71,6 +74,10 @@ public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ct J.Assignment value = annotationAssignments.remove("value"); annotationAssignments.put("name", value.withVariable(((J.Identifier) value.getVariable()).withSimpleName("name"))); } + + // Handle 'reference' attribute migration + handleReferenceAttribute(annotation, annotationAssignments, ctx); + getCursor().putMessageOnFirstEnclosing(J.ClassDeclaration.class, API_MODEL_FQN, annotationAssignments); } else if (SCHEMA_MATCHER.matches(annotation)) { getCursor().putMessageOnFirstEnclosing(J.ClassDeclaration.class, SCHEMA_FQN, "USING SCHEMA ALREADY"); @@ -80,6 +87,44 @@ public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ct return annotation; } + /** + * Handle the 'reference' attribute from @ApiModel. + * - If the value looks like a URL, rename to 'ref' + * - If the value looks like a class name, schedule conversion to 'implementation = ClassName.class' + */ + private void handleReferenceAttribute(J.Annotation annotation, Map annotationAssignments, ExecutionContext ctx) { + if (!annotationAssignments.containsKey("reference")) { + return; + } + + J.Assignment referenceAssignment = annotationAssignments.get("reference"); + Expression assignmentExpr = referenceAssignment.getAssignment(); + + if (!(assignmentExpr instanceof J.Literal)) { + // Not a string literal, leave as-is (will likely fail compilation anyway) + return; + } + + J.Literal literal = (J.Literal) assignmentExpr; + if (literal.getValue() == null) { + return; + } + + String referenceValue = literal.getValue().toString(); + + if (referenceValue.contains("://")) { + // It's a URL - rename 'reference' to 'ref' + // Use SCHEMA_FQN because ChangeType runs before this, converting to @Schema + doAfterVisit(new ChangeAnnotationAttributeName(SCHEMA_FQN, "reference", "ref").getVisitor()); + } else { + // It's a class name - schedule a visitor to convert to 'implementation = ClassName.class' + // This runs after ChangeType converts @ApiModel to @Schema + doAfterVisit(typeReferenceVisitor(referenceValue)); + } + // Remove from map so it won't be added again during merge + annotationAssignments.remove("reference"); + } + @Override public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); @@ -108,4 +153,54 @@ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, Ex } ); } + + private JavaIsoVisitor typeReferenceVisitor(String referenceValue) { + return new JavaIsoVisitor() { + @Override + public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) { + J.Annotation ann = super.visitAnnotation(annotation, ctx); + + if (!SCHEMA_MATCHER.matches(ann) || ann.getArguments() == null) { + return ann; + } + + boolean hasReference = ann.getArguments().stream() + .filter(arg -> arg instanceof J.Assignment) + .map(arg -> (J.Assignment) arg) + .anyMatch(assign -> assign.getVariable() instanceof J.Identifier && + "reference".equals(((J.Identifier) assign.getVariable()).getSimpleName())); + + if (!hasReference) { + return ann; + } + + // Remove the 'reference' attribute + List filteredArgs = ListUtils.map(ann.getArguments(), arg -> { + if (arg instanceof J.Assignment) { + J.Assignment assign = (J.Assignment) arg; + if (assign.getVariable() instanceof J.Identifier && + "reference".equals(((J.Identifier) assign.getVariable()).getSimpleName())) { + return null; + } + } + return arg; + }); + + // Build the new annotation with 'implementation = ClassName.class' + String args = filteredArgs.isEmpty() ? "" : + filteredArgs.stream() + .map(Object::toString) + .reduce((a, b) -> a + ", " + b) + .orElse(""); + String template = args.isEmpty() ? + "implementation = " + referenceValue + ".class" : + args + ", implementation = " + referenceValue + ".class"; + + return JavaTemplate.builder(template) + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "swagger-annotations")) + .build() + .apply(getCursor(), ann.getCoordinates().replaceArguments()); + } + }; + } } diff --git a/src/test/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchemaTest.java b/src/test/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchemaTest.java index dc8d0b1..c23d384 100644 --- a/src/test/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchemaTest.java +++ b/src/test/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchemaTest.java @@ -121,4 +121,50 @@ class Example { ) ); } + + @Test + void shouldMigrateUrlReferenceToRef() { + rewriteRun( + //language=java + java( + """ + import io.swagger.annotations.ApiModel; + + @ApiModel(reference = "https://example.com/schemas/MySchema") + class Example { + } + """, + """ + import io.swagger.v3.oas.annotations.media.Schema; + + @Schema(ref = "https://example.com/schemas/MySchema") + class Example { + } + """ + ) + ); + } + + @Test + void shouldMigrateClassReferenceToImplementation() { + rewriteRun( + //language=java + java( + """ + import io.swagger.annotations.ApiModel; + + @ApiModel(reference = "String") + class Example { + } + """, + """ + import io.swagger.v3.oas.annotations.media.Schema; + + @Schema(implementation = String.class) + class Example { + } + """ + ) + ); + } } From d896972763d634be6f4e90c564ccce3494d1094f Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 12 Jan 2026 14:42:26 +0100 Subject: [PATCH 2/4] Simplify `typeReferenceVisitor` --- .../swagger/MigrateApiModelToSchema.java | 50 ++++--------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java b/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java index 0b584ff..644e0fe 100644 --- a/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java +++ b/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java @@ -15,18 +15,9 @@ */ package org.openrewrite.openapi.swagger; -import org.jspecify.annotations.NonNull; -import org.openrewrite.ExecutionContext; -import org.openrewrite.Preconditions; -import org.openrewrite.Recipe; -import org.openrewrite.TreeVisitor; +import org.openrewrite.*; import org.openrewrite.internal.ListUtils; -import org.openrewrite.java.AnnotationMatcher; -import org.openrewrite.java.ChangeAnnotationAttributeName; -import org.openrewrite.java.ChangeType; -import org.openrewrite.java.JavaIsoVisitor; -import org.openrewrite.java.JavaParser; -import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.*; import org.openrewrite.java.search.UsesType; import org.openrewrite.java.tree.Expression; import org.openrewrite.java.tree.J; @@ -159,47 +150,24 @@ private JavaIsoVisitor typeReferenceVisitor(String referenceVa @Override public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) { J.Annotation ann = super.visitAnnotation(annotation, ctx); - if (!SCHEMA_MATCHER.matches(ann) || ann.getArguments() == null) { return ann; } - boolean hasReference = ann.getArguments().stream() - .filter(arg -> arg instanceof J.Assignment) - .map(arg -> (J.Assignment) arg) - .anyMatch(assign -> assign.getVariable() instanceof J.Identifier && - "reference".equals(((J.Identifier) assign.getVariable()).getSimpleName())); - - if (!hasReference) { - return ann; - } - - // Remove the 'reference' attribute - List filteredArgs = ListUtils.map(ann.getArguments(), arg -> { + // Replace the 'reference' attribute with 'implementation = ClassName.class' + return ann.withArguments(ListUtils.map(ann.getArguments(), arg -> { if (arg instanceof J.Assignment) { J.Assignment assign = (J.Assignment) arg; if (assign.getVariable() instanceof J.Identifier && "reference".equals(((J.Identifier) assign.getVariable()).getSimpleName())) { - return null; + return JavaTemplate.builder("implementation = " + referenceValue + ".class") + .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "swagger-annotations")) + .build() + .apply(new Cursor(getCursor(), arg), arg.getCoordinates().replace()); } } return arg; - }); - - // Build the new annotation with 'implementation = ClassName.class' - String args = filteredArgs.isEmpty() ? "" : - filteredArgs.stream() - .map(Object::toString) - .reduce((a, b) -> a + ", " + b) - .orElse(""); - String template = args.isEmpty() ? - "implementation = " + referenceValue + ".class" : - args + ", implementation = " + referenceValue + ".class"; - - return JavaTemplate.builder(template) - .javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "swagger-annotations")) - .build() - .apply(getCursor(), ann.getCoordinates().replaceArguments()); + })); } }; } From 7f8bbcba6a1e4c39b44a68f314ce032c286eb853 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 12 Jan 2026 14:45:37 +0100 Subject: [PATCH 3/4] Remove unused arguments --- .../openapi/swagger/MigrateApiModelToSchema.java | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java b/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java index 644e0fe..65384bd 100644 --- a/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java +++ b/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java @@ -67,7 +67,7 @@ public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ct } // Handle 'reference' attribute migration - handleReferenceAttribute(annotation, annotationAssignments, ctx); + handleReferenceAttribute(annotationAssignments); getCursor().putMessageOnFirstEnclosing(J.ClassDeclaration.class, API_MODEL_FQN, annotationAssignments); } else if (SCHEMA_MATCHER.matches(annotation)) { @@ -83,26 +83,20 @@ public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ct * - If the value looks like a URL, rename to 'ref' * - If the value looks like a class name, schedule conversion to 'implementation = ClassName.class' */ - private void handleReferenceAttribute(J.Annotation annotation, Map annotationAssignments, ExecutionContext ctx) { + private void handleReferenceAttribute(Map annotationAssignments) { if (!annotationAssignments.containsKey("reference")) { return; } J.Assignment referenceAssignment = annotationAssignments.get("reference"); - Expression assignmentExpr = referenceAssignment.getAssignment(); - - if (!(assignmentExpr instanceof J.Literal)) { - // Not a string literal, leave as-is (will likely fail compilation anyway) - return; + if (!(referenceAssignment.getAssignment() instanceof J.Literal)) { + return; // Not a string literal, leave as-is (will likely fail compilation anyway) } - - J.Literal literal = (J.Literal) assignmentExpr; + J.Literal literal = (J.Literal) referenceAssignment.getAssignment(); if (literal.getValue() == null) { return; } - String referenceValue = literal.getValue().toString(); - if (referenceValue.contains("://")) { // It's a URL - rename 'reference' to 'ref' // Use SCHEMA_FQN because ChangeType runs before this, converting to @Schema From 9eb89895adc0f0133b99ef793ba1ac97a4852bac Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 12 Jan 2026 18:24:15 +0100 Subject: [PATCH 4/4] Also support local references --- .../swagger/MigrateApiModelToSchema.java | 2 +- .../swagger/MigrateApiModelToSchemaTest.java | 20 +++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java b/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java index 65384bd..0ea1c16 100644 --- a/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java +++ b/src/main/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchema.java @@ -97,7 +97,7 @@ private void handleReferenceAttribute(Map annotationAssign return; } String referenceValue = literal.getValue().toString(); - if (referenceValue.contains("://")) { + if (referenceValue.contains("://") || referenceValue.contains("#")) { // It's a URL - rename 'reference' to 'ref' // Use SCHEMA_FQN because ChangeType runs before this, converting to @Schema doAfterVisit(new ChangeAnnotationAttributeName(SCHEMA_FQN, "reference", "ref").getVisitor()); diff --git a/src/test/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchemaTest.java b/src/test/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchemaTest.java index c23d384..2c82182 100644 --- a/src/test/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchemaTest.java +++ b/src/test/java/org/openrewrite/openapi/swagger/MigrateApiModelToSchemaTest.java @@ -131,14 +131,30 @@ void shouldMigrateUrlReferenceToRef() { import io.swagger.annotations.ApiModel; @ApiModel(reference = "https://example.com/schemas/MySchema") - class Example { + class RemoteReference { } """, """ import io.swagger.v3.oas.annotations.media.Schema; @Schema(ref = "https://example.com/schemas/MySchema") - class Example { + class RemoteReference { + } + """ + ), + java( + """ + import io.swagger.annotations.ApiModel; + + @ApiModel(reference = "#/schemas/MySchema") + class LocalReference { + } + """, + """ + import io.swagger.v3.oas.annotations.media.Schema; + + @Schema(ref = "#/schemas/MySchema") + class LocalReference { } """ )