diff --git a/fern/products/sdks/generators/java/migration-v5.mdx b/fern/products/sdks/generators/java/migration-v5.mdx new file mode 100644 index 000000000..1037c6859 --- /dev/null +++ b/fern/products/sdks/generators/java/migration-v5.mdx @@ -0,0 +1,298 @@ +--- +title: Migrating to Java generator v5 +headline: Migrating to Java SDK generator v5 +description: Breaking changes in version 5.0.0 of the Fern Java SDK generator and how to update code that uses your generated Java SDK. +--- + +Version 5.0.0 of the Java SDK generator renames many generated types, changes pagination and error handling, and generates more precise field types. Release the regenerated SDK as a new major version, and share this page with your SDK users so they can update their code. + +The changes below are ordered from most to least likely to affect you. Compile existing code against the regenerated SDK first; the compiler lists most of the changes you need to make. + +## Before you upgrade + +Generator versions 5.0.0 and later read configuration from `sdk-config.yml` instead of `generators.yml`. Run `fern sdk migrate` to create `sdk-config.yml` from your existing configuration. + +## Type names and imports change + +This change affects every SDK and causes the most compile errors, so fix it first. + +Most request wrapper names are unchanged. The `idiomatic-request-names` option defaults to `true`, which produces the same verb-first names as the previous generator. Set it to `false` to use resource-first names. When the API definition doesn't name a request body, the generated name may leave out the resource name: + +```java +// Before +GetByV1IdPlantsRequest + +// After +GetByV1IdRequest +``` + +Core exception types include the client name: + +```java +// Before // After +PlantStoreException PlantStoreClientException +PlantStoreApiException PlantStoreClientApiException +``` + +Packages below the root package move. Per-resource `types` packages are merged into the resource package, and error classes move to a single `errors` package at the root. + +```java +// Before +import com.plantstore.api.resources.inventory.plants.types.PlantStatus; +import com.plantstore.api.resources.orders.errors.NotFoundError; + +// After +import com.plantstore.api.resources.inventory.types.PlantStatus; +import com.plantstore.api.errors.NotFoundError; +``` + + +If you don't set `package-prefix`, the root package can change. Set `package-prefix` to your current root package, such as `com.plantstore.api`, to keep it. + + +**To fix:** Rename types, then remove generated-package imports and let your IDE resolve them again. A `cannot find symbol` error for a type you used before usually means the type has a different name. + +## Paginated methods return a pager + +List endpoints return an iterable pager that fetches the next page as you iterate, instead of a single page. Async methods return a `CompletableFuture` that resolves to a pager. + +```java +// Before +ListPlantsResponse page = client.plants().list(); +for (Plant plant : page.getPlants()) { + process(plant); +} +Optional next = page.getNext(); +while (next.isPresent()) { + page = client.plants().list(ListPlantsRequest.builder().cursor(next.get()).build()); + for (Plant plant : page.getPlants()) { + process(plant); + } + next = page.getNext(); +} + +// After +for (Plant plant : client.plants().list()) { + process(plant); +} +``` + +**To fix:** Replace cursor loops with iteration. Call `getResponse()` to read the raw response, such as a cursor or total count. + +## Responses are validated against the schema + +Schema constraints such as `pattern`, `minLength`, `minimum`, `maximum`, `minItems`, and `uniqueItems` are enforced when responses are decoded, including collections and paginated items. A response that violates the schema throws an exception that names the invalid field. Request validation remains off by default. + +**To fix:** Run integration tests against the regenerated SDK. If the API returns responses that violate the schema, fix the schema. To unblock yourself in the meantime, set `validateResponses: false`. + +## Builders require different fields + +Staged builders read required fields from the spec. A field the spec marks as required becomes a required builder stage, even if the previous generator treated it as optional. + +```java +// Before +CareSchedule.builder() + .plantId(plantId) + .build(); + +// After +CareSchedule.builder() + .plantId(plantId) + .interval(interval) + .build(); +``` + +**To fix:** Supply the required fields. If a field isn't required by the API, mark it as optional in the spec. + +## Exceptions are organized by HTTP status + +The SDK generates the standard set of status-named exceptions even if the spec doesn't declare them, plus one SDK exception for unmapped statuses. An exception declared for a nonstandard status gets a generic name, and exception messages change. + +```java +// Before +try { + client.plants().create(request); +} catch (PlantDormantError e) { // spec-declared name for HTTP 499 + wakePlant(); +} + +// After +try { + client.plants().create(request); +} catch (Status499Error e) { + wakePlant(); +} +``` + +**To fix:** Check every `catch` that names a generated exception, and replace checks on exception message text with checks on the status code. + +## Dates and integers use specific types + +Fields with `format: date` or `format: date-time` are `LocalDate` and `OffsetDateTime` instead of `String`. Integer fields use the width of their format, so `int64` fields are `Long` instead of `Integer`. + +```java +// Before +OffsetDateTime wateredAt = OffsetDateTime.parse(plant.getWateredAt()); +Integer priceCents = plant.getPriceCents(); + +// After +OffsetDateTime wateredAt = plant.getWateredAt(); +Long priceCents = plant.getPriceCents(); +``` + +**To fix:** Remove date parsing code, and change `Integer` to `Long` where the compiler requires it. + +## String fields with a declared enum become enums + +A string field whose spec declares an `enum` list is a Java `enum` instead of a `String`. + +```java +// Before +if ("AVAILABLE".equals(plant.getStatus())) { } + +// After +if (plant.getStatus() == PlantStatus.AVAILABLE) { } +``` + +**To fix:** Compare against enum constants. `toString()` returns the wire value. + +## File uploads are set on the request object + +On file-upload endpoints, the file is a field on the request object instead of a separate method parameter. The file is a `FileStream`, which includes its filename and content type. + +```java +// Before +client.photos().upload(Optional.of(photoFile)); +client.photos().upload(Optional.of(photoFile), inputStream, "monstera.png"); + +// After +client.photos().upload(UploadPhotosRequest.builder() + .photoFile(new FileStream(inputStream, "monstera.png", null)) + .build()); +``` + +**To fix:** Set the file on the request builder, and pass the input stream and filename to `FileStream`. + +## `label` and `matrix` path parameters are encoded + +Path parameters that use the OpenAPI `label` or `matrix` style are encoded as the spec defines instead of as plain path segments. The `simple` style is unchanged. + +```none +// Before +GET /plants/42/shade/indoor + +// After +GET /plants/42/.shade/;location=indoor +``` + +**To fix:** Confirm that your API's routing accepts the encoded form. + +## Build files and documentation change + +The SDK still includes `build.gradle` and `settings.gradle`, but doesn't include the Gradle wrapper or the Spotless plugin. Java compatibility is set in the `java {}` block, which supports Gradle 8 and 9. + +```groovy +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} +``` + +The per-resource `documentation/` directory is removed. The SDK root includes `README.md`, `reference.md`, which lists every endpoint with its signature and an example, and `CONTRIBUTING.md`. + +Tests for the SDK's runtime helpers aren't generated. To generate tests for your API, turn on `generateTests`. The generated suite includes a serialization round-trip test for each model and a wire test for each endpoint that uses `MockWebServer`. The build file adds `junit-jupiter`, `junit-platform-launcher`, `mockwebserver`, and a `test { useJUnitPlatform() }` task. + +**To fix:** Check in your own Gradle wrapper, and update links to `documentation/` to point to `reference.md`. Generated tests don't cover behavior that depends on your server, so add integration tests that cover response validation and exception handling. + +## WebSocket channels + +The following changes apply to SDKs with WebSocket channels. + +### Credentials are required to connect + +Credentials declared by the channel's security scheme are sent with the WebSocket handshake. If you don't pass a credential, it's read from the scheme's environment variable. If neither is set, `connect()` throws before opening the socket. An explicit header takes precedence. + +```java +var client = PlantStoreClient.builder().apiKey("abc123").build(); +var fromEnv = PlantStoreClient.builder().build(); // reads PLANTSTORE_API_KEY + +PlantStoreClient.builder().build().growth().connect(); +// RuntimeException: Please provide apiKey or set the PLANTSTORE_API_KEY environment variable. +``` + +**To fix:** Set the credential if you open sockets without one. + +### The two type packages swap contents + +Frame models move from the per-channel `resources...types` package to the shared `types` package, and parameter enums move the other way. + +```java +// Before +import com.plantstore.api.resources.growth.v1.types.GrowthV1Reading; +import com.plantstore.api.types.GrowthV1Mode; + +// After +import com.plantstore.api.types.GrowthV1Reading; +import com.plantstore.api.resources.growth.v1.types.GrowthV1Mode; +``` + +**To fix:** Move frame model imports to the shared package and parameter enum imports to the per-channel package. Moving every import to one package fixes half the errors and causes the other half. + +### The discriminator is a typed field + +Frame models declare the `type` field. `getType()` still returns a `String`, as before. What changed: `type` is no longer also collected into the additional-properties map. Code that reads `type` from the additional-properties map gets `null`. + +```java +// Before +String kind = reading.getType(); // "Reading" +String sameKind = (String) reading.getAdditionalProperties().get("type"); // "Reading" + +// After +String kind = reading.getType(); // "Reading" +Object sameKind = reading.getAdditionalProperties().get("type"); // null +``` + +**To fix:** Nothing, if you already read the discriminator from `getType()`. Check any code that iterates the additional-properties map expecting `type` to be there. + +### Parameter wrapper types become `String` + +Connect parameters that had a wrapper class with an `of(...)` factory are `String`. Enum parameters keep their types. The query string sent is unchanged. + +```java +// Before +GrowthV1ConnectOptions.builder() + .label(GrowthV1Label.of("greenhouse")) + .intervalMs(GrowthV1IntervalMs.of(16000)) + .build(); + +// After +GrowthV1ConnectOptions.builder() + .label("greenhouse") + .intervalMs(String.valueOf(16000)) + .build(); +``` + +**To fix:** Replace each `Wrapper.of(value)` call with the value's string form. + +### Reconnection reports exhaustion + +When the SDK runs out of reconnection attempts, it passes an `IllegalStateException` to your error handler instead of stopping silently. The same `maxRetries` value makes one more attempt than before. + +```java +socket.onError(e -> { + if (e instanceof IllegalStateException + && String.valueOf(e.getMessage()).contains("reconnection attempts exhausted")) { + escalate(); + return; + } + log.error("socket error", e); +}); +``` + +**To fix:** Handle the exhaustion case in your error handler, and reduce `maxRetries` by one if you depend on the exact number of attempts. + +### `disconnect()` before connecting doesn't throw + +Calling `disconnect()` on a socket that was never connected returns normally instead of throwing a `NullPointerException`. + +**To fix:** No changes are required. You can remove guards around `disconnect()`. diff --git a/fern/products/sdks/sdks.yml b/fern/products/sdks/sdks.yml index 9ddca8f38..945980069 100644 --- a/fern/products/sdks/sdks.yml +++ b/fern/products/sdks/sdks.yml @@ -113,6 +113,10 @@ navigation: - page: Adding custom code path: ./generators/java/custom-code.mdx slug: custom-code + - page: Migrating to v5 + path: ./generators/java/migration-v5.mdx + slug: migration-v5 + hidden: true - changelog: ./generators/java/changelog slug: changelog - link: Customer showcase