From 0c8c91dd79ef14c9173758678542ca81bc14e563 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:23:54 +0000 Subject: [PATCH 1/3] Add java SDK generator v5 migration guide --- .../sdks/generators/java/migration-v5.mdx | 385 ++++++++++++++++++ fern/products/sdks/sdks.yml | 4 + 2 files changed, 389 insertions(+) create mode 100644 fern/products/sdks/generators/java/migration-v5.mdx 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 0000000000..1ef40f00ff --- /dev/null +++ b/fern/products/sdks/generators/java/migration-v5.mdx @@ -0,0 +1,385 @@ +--- +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. + +Generated request and response types are named resource first instead of verb first: + +```java +// Before +client.plants().get(id, GetPlantsRequest.builder().build()); + +// After +client.plants().get(id, PlantsGetRequest.builder().build()); +``` + +Core exception types include the client name: + +```java +// Before // After +PlantStoreException PlantStoreClientException +PlantStoreApiException PlantStoreClientApiException +``` + +The root package is unchanged, but packages below it 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; +``` + +**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 resource-first 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. + +## Nullable fields become `Optional` + +Fields that could be explicitly `null` change from `OptionalNullable` to `Optional`, which can't distinguish between an unset value and `null`. An empty `Optional` omits the field from the request instead of sending `null`. Explicitly null query parameters and headers are also omitted. + +```java +// Before +UpdatePlantRequest.builder() + .nickname(Nullable.of(null)) + .build(); +// Sends {"nickname": null}, and the server clears the field + +// After +UpdatePlantRequest.builder() + .nickname(Optional.empty()) + .build(); +// Sends {}, and the server leaves the field unchanged +``` + + +Check update requests, such as `PATCH` calls, that send `null` to clear a field. They compile after you switch to `Optional`, but no longer clear the field. + + +## 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. + +## Free-form object fields are `Object` + +Untyped object fields, such as metadata, are `Object` instead of `Map`, because the spec doesn't guarantee that the value is a map. + +```java +// Before +Map metadata = plant.getMetadata(); + +// After +PlantMetadata metadata = objectMapper.convertValue(plant.getMetadata(), PlantMetadata.class); +``` + +**To fix:** Cast or deserialize the value where you use it, or declare the field's shape in the spec. + +## 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 change, and no tests are generated + +The SDK still includes `build.gradle` and `settings.gradle`, but doesn't include the Gradle wrapper, the Spotless plugin, or the generated tests for SDK runtime helpers. 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 +} +``` + +**To fix:** Check in your own Gradle wrapper, and add integration tests that cover nullable fields, 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. + +### Nested types lose their owner prefix + +A type nested in a frame is named after itself instead of the frame that contains it, and moves to the shared package. Field shapes are unchanged. + +```java +// Before +import com.plantstore.api.resources.growth.v1.types.GrowthV1ReadingSensor; + +// After +import com.plantstore.api.types.Sensor; +``` + +**To fix:** Remove the owner prefix and update the import. + +### The socket client and message handlers are renamed + +The per-channel socket client, its accessor, and message handlers include the channel name. Lifecycle handlers (connected, disconnected, error, and raw message), `disconnect`, `close`, `getReadyState`, and reconnect options keep their names. + +```java +// Before +V1WebSocketClient socket = client.growth().v1().v1WebSocket(); +socket.onReading(this::handleReading); + +// After +GrowthV1WebSocketClient socket = client.growth().v1().growthV1WebSocket(); +socket.onGrowthV1Reading(this::handleReading); +``` + +**To fix:** Rename the client type, accessor, and message handlers. + +### The discriminator is a declared field + +Frame models declare the `type` field, so it's no longer in the additional-properties map. + +```java +// Before +String kind = (String) reading.getAdditionalProperties().get("type"); + +// After +String kind = reading.getType(); +``` + +**To fix:** Read the discriminator from `getType()`. + +### Control-frame types are merged + +Frames that share a definition are generated as one type with a `type` discriminator. For example, separate keep-alive and close-stream types are removed. The payload on the wire is unchanged. Some receive-only frame types are removed and their handlers have no replacement. + +```java +// Before +socket.sendKeepAlive(GrowthV1KeepAlive.builder().build()); + +// After +socket.sendKeepAlive(GrowthV1Finalize.builder() + .type(GrowthV1FinalizeType.KEEP_ALIVE) + .build()); +``` + +Some send-frame builders that previously set `type` automatically also require it: + +```java +socket.sendNote(GrowthV1Note.builder() + .type(GrowthV1NoteType.NOTE) + .text("New leaf") + .build()); +``` + +**To fix:** Replace each removed type with the merged type, and set `type` on builders that require it. Remove handlers for receive events that no longer exist. + +### 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. + +### The OkHttp dependency uses an earlier major version + +The SDK depends on an earlier major version of OkHttp. If your project or another dependency uses a later major version, the resolved version can cause a `NoSuchMethodError` at runtime. + +**To fix:** Run `gradle dependencies` to check the resolved version, and add a dependency constraint if needed. + +### 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 9ddca8f38d..9459800697 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 From ec4b903dbedc824cd12fce51aaa575804062d2db Mon Sep 17 00:00:00 2001 From: Nermina Date: Thu, 1 Oct 2026 11:20:36 +0000 Subject: [PATCH 2/3] Update Java migration guide to match latest internal guide Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdks/generators/java/migration-v5.mdx | 126 ++++-------------- 1 file changed, 29 insertions(+), 97 deletions(-) diff --git a/fern/products/sdks/generators/java/migration-v5.mdx b/fern/products/sdks/generators/java/migration-v5.mdx index 1ef40f00ff..ec844ad80a 100644 --- a/fern/products/sdks/generators/java/migration-v5.mdx +++ b/fern/products/sdks/generators/java/migration-v5.mdx @@ -16,14 +16,14 @@ Generator versions 5.0.0 and later read configuration from `sdk-config.yml` inst This change affects every SDK and causes the most compile errors, so fix it first. -Generated request and response types are named resource first instead of verb 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 -client.plants().get(id, GetPlantsRequest.builder().build()); +GetByV1IdPlantsRequest // After -client.plants().get(id, PlantsGetRequest.builder().build()); +GetByV1IdRequest ``` Core exception types include the client name: @@ -34,7 +34,7 @@ PlantStoreException PlantStoreClientException PlantStoreApiException PlantStoreClientApiException ``` -The root package is unchanged, but packages below it move. Per-resource `types` packages are merged into the resource package, and error classes move to a single `errors` package at the root. +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 @@ -46,7 +46,11 @@ import com.plantstore.api.resources.inventory.types.PlantStatus; import com.plantstore.api.errors.NotFoundError; ``` -**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 resource-first name. + +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 @@ -75,28 +79,6 @@ for (Plant plant : client.plants().list()) { **To fix:** Replace cursor loops with iteration. Call `getResponse()` to read the raw response, such as a cursor or total count. -## Nullable fields become `Optional` - -Fields that could be explicitly `null` change from `OptionalNullable` to `Optional`, which can't distinguish between an unset value and `null`. An empty `Optional` omits the field from the request instead of sending `null`. Explicitly null query parameters and headers are also omitted. - -```java -// Before -UpdatePlantRequest.builder() - .nickname(Nullable.of(null)) - .build(); -// Sends {"nickname": null}, and the server clears the field - -// After -UpdatePlantRequest.builder() - .nickname(Optional.empty()) - .build(); -// Sends {}, and the server leaves the field unchanged -``` - - -Check update requests, such as `PATCH` calls, that send `null` to clear a field. They compile after you switch to `Optional`, but no longer clear the field. - - ## 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. @@ -174,20 +156,6 @@ if (plant.getStatus() == PlantStatus.AVAILABLE) { } **To fix:** Compare against enum constants. `toString()` returns the wire value. -## Free-form object fields are `Object` - -Untyped object fields, such as metadata, are `Object` instead of `Map`, because the spec doesn't guarantee that the value is a map. - -```java -// Before -Map metadata = plant.getMetadata(); - -// After -PlantMetadata metadata = objectMapper.convertValue(plant.getMetadata(), PlantMetadata.class); -``` - -**To fix:** Cast or deserialize the value where you use it, or declare the field's shape in the spec. - ## 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. @@ -219,9 +187,9 @@ GET /plants/42/.shade/;location=indoor **To fix:** Confirm that your API's routing accepts the encoded form. -## Build files change, and no tests are generated +## Build files and documentation change -The SDK still includes `build.gradle` and `settings.gradle`, but doesn't include the Gradle wrapper, the Spotless plugin, or the generated tests for SDK runtime helpers. Java compatibility is set in the `java {}` block, which supports Gradle 8 and 9. +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 { @@ -230,7 +198,11 @@ java { } ``` -**To fix:** Check in your own Gradle wrapper, and add integration tests that cover nullable fields, response validation, and exception handling. +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 @@ -266,74 +238,40 @@ 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. -### Nested types lose their owner prefix - -A type nested in a frame is named after itself instead of the frame that contains it, and moves to the shared package. Field shapes are unchanged. - -```java -// Before -import com.plantstore.api.resources.growth.v1.types.GrowthV1ReadingSensor; - -// After -import com.plantstore.api.types.Sensor; -``` - -**To fix:** Remove the owner prefix and update the import. +### The discriminator is a typed field -### The socket client and message handlers are renamed - -The per-channel socket client, its accessor, and message handlers include the channel name. Lifecycle handlers (connected, disconnected, error, and raw message), `disconnect`, `close`, `getReadyState`, and reconnect options keep their names. +Frame models declare the `type` field, so it's no longer in the additional-properties map, and `getType()` returns an enum instead of a `String`. Code that reads `type` from the additional-properties map gets `null`. ```java // Before -V1WebSocketClient socket = client.growth().v1().v1WebSocket(); -socket.onReading(this::handleReading); - -// After -GrowthV1WebSocketClient socket = client.growth().v1().growthV1WebSocket(); -socket.onGrowthV1Reading(this::handleReading); -``` - -**To fix:** Rename the client type, accessor, and message handlers. - -### The discriminator is a declared field - -Frame models declare the `type` field, so it's no longer in the additional-properties map. - -```java -// Before -String kind = (String) reading.getAdditionalProperties().get("type"); +String kind = reading.getType(); +String sameKind = (String) reading.getAdditionalProperties().get("type"); // After -String kind = reading.getType(); +GrowthV1ReadingType kind = reading.getType(); +String wireValue = reading.getType().toString(); // "Reading" ``` -**To fix:** Read the discriminator from `getType()`. +**To fix:** Read the discriminator from `getType()`, call `toString()` where you need the wire value, and check code that iterates the additional-properties map. -### Control-frame types are merged +### Send frames require the discriminator -Frames that share a definition are generated as one type with a `type` discriminator. For example, separate keep-alive and close-stream types are removed. The payload on the wire is unchanged. Some receive-only frame types are removed and their handlers have no replacement. +Some send-frame builders previously set `type` automatically. These builders require you to set it. ```java // Before -socket.sendKeepAlive(GrowthV1KeepAlive.builder().build()); - -// After -socket.sendKeepAlive(GrowthV1Finalize.builder() - .type(GrowthV1FinalizeType.KEEP_ALIVE) +socket.sendNote(GrowthV1Note.builder() + .text("New leaf") .build()); -``` - -Some send-frame builders that previously set `type` automatically also require it: -```java +// After socket.sendNote(GrowthV1Note.builder() .type(GrowthV1NoteType.NOTE) .text("New leaf") .build()); ``` -**To fix:** Replace each removed type with the merged type, and set `type` on builders that require it. Remove handlers for receive events that no longer exist. +**To fix:** Add the `type` call to each affected builder. ### Parameter wrapper types become `String` @@ -355,12 +293,6 @@ GrowthV1ConnectOptions.builder() **To fix:** Replace each `Wrapper.of(value)` call with the value's string form. -### The OkHttp dependency uses an earlier major version - -The SDK depends on an earlier major version of OkHttp. If your project or another dependency uses a later major version, the resolved version can cause a `NoSuchMethodError` at runtime. - -**To fix:** Run `gradle dependencies` to check the resolved version, and add a dependency constraint if needed. - ### 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. From 71d0ce84e2e8d57a126e15cf51361d44530245fe Mon Sep 17 00:00:00 2001 From: shraddha-postman Date: Mon, 5 Oct 2026 08:50:33 -0700 Subject: [PATCH 3/3] Fix the WebSocket discriminator section against Fern's real v5 output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getType() returns String, not an enum, before and after; only the additional-properties-map behavior changed - removed 'Send frames require the discriminator' — no send-frame builder in Fern's generated output requires it; verified against generated output across three customers, not just one example Co-Authored-By: Claude --- .../sdks/generators/java/migration-v5.mdx | 31 ++++--------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/fern/products/sdks/generators/java/migration-v5.mdx b/fern/products/sdks/generators/java/migration-v5.mdx index ec844ad80a..1037c68595 100644 --- a/fern/products/sdks/generators/java/migration-v5.mdx +++ b/fern/products/sdks/generators/java/migration-v5.mdx @@ -240,38 +240,19 @@ import com.plantstore.api.resources.growth.v1.types.GrowthV1Mode; ### The discriminator is a typed field -Frame models declare the `type` field, so it's no longer in the additional-properties map, and `getType()` returns an enum instead of a `String`. Code that reads `type` from the additional-properties map gets `null`. +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(); -String sameKind = (String) reading.getAdditionalProperties().get("type"); +String kind = reading.getType(); // "Reading" +String sameKind = (String) reading.getAdditionalProperties().get("type"); // "Reading" // After -GrowthV1ReadingType kind = reading.getType(); -String wireValue = reading.getType().toString(); // "Reading" +String kind = reading.getType(); // "Reading" +Object sameKind = reading.getAdditionalProperties().get("type"); // null ``` -**To fix:** Read the discriminator from `getType()`, call `toString()` where you need the wire value, and check code that iterates the additional-properties map. - -### Send frames require the discriminator - -Some send-frame builders previously set `type` automatically. These builders require you to set it. - -```java -// Before -socket.sendNote(GrowthV1Note.builder() - .text("New leaf") - .build()); - -// After -socket.sendNote(GrowthV1Note.builder() - .type(GrowthV1NoteType.NOTE) - .text("New leaf") - .build()); -``` - -**To fix:** Add the `type` call to each affected builder. +**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`