Skip to content

Keep the client compatible across ThingsBoard OpenAPI type corrections - #86

Merged
ViacheslavKlimov merged 6 commits into
thingsboard:masterfrom
vvlladd28:feature/openapi-compat-layer
Nov 9, 2026
Merged

ViacheslavKlimov merged 6 commits into
thingsboard:masterfrom
vvlladd28:feature/openapi-compat-layer

Conversation

@vvlladd28

@vvlladd28 vvlladd28 commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

What

Keeps code written against thingsboard-*-client 4.4.0 compiling once the client is regenerated from specs with the type corrections of thingsboard/thingsboard#16561. It works with the current specs too, so it can be merged before the spec update.

  • scripts/apply_client_compat.py (run by generate-client.sh before generating PE and PaaS) copies the spec to <edition>/target/openapi-client.json with vendor extensions from openapi/client-compat.json:
    • legacyStringParams: pageSize/page of the 28 PE/PaaS operations that 4.4.0 took as String (getUserAssets, getEntityGroupsByTypeAndPageLink, getIntegrations, ...). Once the spec documents them as int32, they get x-legacy-string, and api.mustache adds deprecated overloads that convert and delegate:
      • Builder pageSize(String) / page(String) in the *Args builders;
      • a positional overload with String for these parameters;
      • a positional <operation>WithHttpInfo(..., String pageSize, String page, ..., Map<String, String> headers) overload (4.4.0 had these as public methods too).
    • subtypes: {"AlarmInfo": "Alarm"}. On the server AlarmInfo extends Alarm, but the spec describes two unrelated schemas, and #16561 makes assignAlarm / unassignAlarm return AlarmInfo. The script rewrites AlarmInfo into allOf: [Alarm, its own properties] and marks Alarm with x-parent, so AlarmInfo extends Alarm and Alarm a = client.assignAlarm(...) still compiles. This is done here rather than in the server spec because dart-dio turns an allOf parent into an abstract interface (Alarm(...) would no longer compile in Dart). readOnly properties stay in the child: openapi-generator 7.20 generates code that assigns the parent's private fields for inherited readOnly properties.
    • The script fails, naming the config entry, when the spec no longer matches the config: a subtype schema is missing, the child does not have every parent property with the same definition (it lists them), the child already uses allOf in another shape than the script produces, an operationId is not in the spec, or a listed parameter is not a query parameter of it (or is neither a string nor an int32). A parameter still documented as a string is counted as pending: Client compat (pe): AlarmInfo -> Alarm; 28/28 operations, 0/56 params marked, 56 pending (still string).
    • CE is deprecated and its spec is frozen with none of these operations, so CE is generated from the spec unchanged (its AlarmInfo stays as in 4.4.0).
  • pojo.mustache: a child class now also overrides add<Name>Item / put<Name>Item of inherited list and map properties, as it already does for the other fluent setters. Without it, AlarmInfo.addPropagateRelationTypesItem(...) would return Alarm instead of AlarmInfo as in 4.4.0.
  • model/AliasEntityId is hand-written in common/ and replaces the generated model (generate-client.sh already copies common/ on top). The server writes an EntityType or an AliasEntityType name in one entityType field ({"entityType": "CURRENT_USER"}), and #16561 documents it as a string. The class keeps the 4.4.0 API (entityType(EntityType), getEntityType(), aliasEntityType(AliasEntityType), getAliasEntityType(), id(UUID)) on top of that field, so code that uses entity IDs is unchanged, and aliases can now be sent and read. It also leaves out "id": null, which the server rejects for an alias, and keeps JSON_PROPERTY_ALIAS_ENTITY_TYPE as a deprecated constant. setEntityType(null) clears only an entity type and setAliasEntityType(null) only an alias, so copying properties one by one (e.g. BeanUtils.copyProperties) keeps an alias.

Source-incompatible changes that remain after #16561:

  • ThingsboardErrorCode.getValue(): Integer instead of BigDecimal; the enum constants (NUMBER_2, ...) are the same.
  • *Args.pageSize() / page() accessors of the 28 operations return Integer instead of String (a method cannot be overloaded by return type).
  • A bare null for pageSize / page (pageSize(null), getUserAssets(null, null, ...)) becomes ambiguous between the String and Integer overloads. These parameters are required, so such calls already failed at runtime.

Testing

  • ./generate-client.sh all with the current specs changes only:
    • AliasEntityId (hand-written);
    • AlarmInfo (PE, PaaS), which now extends Alarm. Every public member of the 4.4.0 AlarmInfo is still there, declared or inherited, with the same signature (compared with javap);
    • the add<Name>Item / put<Name>Item overrides in existing child classes.
  • mvn clean package passes for all editions.
  • New AliasEntityIdTest (common): JSON of an entity ID and of an alias, reading server JSON, SingleEntityFilter with an alias, set*(null) keeping a value of the other kind.
  • New ClientCompatibilityTest (PE, PaaS), written against the 4.4.0 API: GetUserAssetsArgs.builder().pageSize("10").page("0"), getUserAssets("10", "0", ...), getUserAssetsWithHttpInfo("10", "0", ..., headers), Alarm a = api.assignAlarm(...) / unassignAlarm(...), AlarmInfo.addPropagateRelationTypesItem(...) returning AlarmInfo, the 4.4.0 AliasEntityId methods. It must keep compiling after regeneration; API calls go to a closed port.
  • apply_client_compat.py on copies of the PE spec with a renamed operation, a renamed parameter, an int64 parameter, an extra/changed Alarm property, a missing Alarm and a differently shaped allOf fails with the entry named; re-running it on its own output is accepted.
  • I applied the #16561 changes (with AlarmInfo left flat, as the server documents it) to the PE spec (56/56 params marked), generated the client and compiled ClientCompatibilityTest and a test that uses the 4.4.0 API (without the new WithHttpInfo overloads, getUserAssetsWithHttpInfo("10", "0", ...) does not compile):
    • GetUserAssetsArgs.builder().pageSize("100").page("0") and getUserAssets("100", "0", ...) compile and convert to Integer;
    • Alarm a = client.assignAlarm(...) compiles; an AlarmInfo read from JSON exposes name and createdTime through an Alarm reference and round-trips;
    • AliasEntityId and ThingsboardErrorCode.NUMBER_10 behave as above.

ThingsBoard corrects OpenAPI types that the generated client got wrong
(thingsboard/thingsboard#16561). Regenerated as is, the client would stop
compiling for code written against 4.4.0. This adds a compatibility layer:

- scripts/apply_client_compat.py copies the spec with vendor extensions
  from openapi/client-compat.json before generation:
  - pageSize/page of 28 operations that 4.4.0 took as String: api.mustache
    adds deprecated String overloads (builder and positional) that
    delegate to the Integer ones;
  - x-parent on Alarm, so AlarmInfo (allOf Alarm) extends Alarm and
    assignAlarm/unassignAlarm results stay assignable to Alarm.
- model/AliasEntityId is hand-written in common/ and replaces the
  generated model. The server writes an EntityType or an AliasEntityType
  name in one entityType field; the class keeps the typed accessors of
  4.4.0 and can now send and read aliases (CURRENT_USER, ...).

With the current specs the generated code is unchanged except for
AliasEntityId.
Documenting AlarmInfo as allOf Alarm in the server spec would turn Alarm
into an abstract interface in the Dart client (dart-dio), so the spec
stays flat. apply_client_compat.py now rewrites listed subtypes
(client-compat.json "subtypes") into allOf [parent, own properties] and
marks the parent with x-parent before generation, so the Java AlarmInfo
extends Alarm with the current specs already.

Child classes now also override add<Name>Item / put<Name>Item of
inherited list and map properties, so these fluent calls keep returning
the child type, as the other inherited fluent setters already do.
AliasEntityId keeps JSON_PROPERTY_ALIAS_ENTITY_TYPE as a deprecated
constant.

@vvlladd28 vvlladd28 left a comment

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.

Review summary

Reviewed 99 changed files in Keep the client compatible across ThingsBoard OpenAPI type corrections. Most of them are regenerated models. I read the hand-written files closely and compared the PE/PaaS output with CE. Left 5 comments inline.

The overall approach holds up. I checked it against the v4.4 tag. The 28 operations in client-compat.json are exactly the PE/PaaS operations that took String pageSize/page in 4.4.0. Every 4.4.0 public AlarmInfo member is still present, declared or inherited. The hand-written AliasEntityId keeps the full 4.4.0 surface.

One real gap: the String overloads cover op(...) and the *Args builders, but not the public positional opWithHttpInfo(String pageSize, String page, ..., Map<String, String> headers). 4.4.0 code that calls those methods will stop compiling after the spec update (openapi/api.mustache:357). The *Args accessors String pageSize() / page() will also change type. Those can't be shimmed, so they belong in the known-breaks note next to ThingsboardErrorCode.getValue().


This review was auto-generated. Findings may contain errors — please verify before applying changes.

Comment thread openapi/api.mustache Resolved
Comment thread openapi/api.mustache Resolved
Comment thread scripts/apply_client_compat.py Resolved
Comment thread scripts/apply_client_compat.py Resolved
4.4.0 also had public positional <operation>WithHttpInfo methods with
String pageSize/page (28 in PE). Operations with x-legacy-string-params
now get a deprecated WithHttpInfo overload that takes String for these
parameters and headers, converts them and delegates to the Integer one.
No generated change with the current specs.
setEntityType(null) cleared an alias set with setAliasEntityType, and
vice versa, since both write the one entityType field. Copying
properties one by one (e.g. BeanUtils: aliasEntityType, then entityType)
lost the alias. Now setEntityType(null) clears only an entity type and
setAliasEntityType(null) only an alias.
…json

apply_client_compat.py silently skipped every mismatch, so a spec change
could drop a compatibility overload unnoticed. It now fails, naming the
config entry, when a subtype schema is missing, the child does not have
every parent property with the same definition (listing them), the child
already uses allOf in another shape than the script produces, an
operation is not in the spec, or a listed parameter is not a query
parameter or is neither a string nor an int32 integer. A parameter still
documented as a string is counted as pending. The summary line shows the
edition, subtypes and marked/pending parameters.

generate-client.sh runs it for PE and PaaS only: CE is deprecated, its
spec is frozen and has none of the listed operations, so CE is generated
from the spec unchanged (AlarmInfo stays as in 4.4.0 there).
ClientCompatibilityTest (PE, PaaS) is written against the 4.4.0 client:
String pageSize/page in the Args builder, the positional method and the
positional WithHttpInfo method, Alarm results of assignAlarm and
unassignAlarm, AlarmInfo fluent setters and the AliasEntityId accessors.
It must keep compiling when the client is regenerated from specs with
corrected types. API calls go to a closed port.
@ViacheslavKlimov
ViacheslavKlimov merged commit c881fcb into thingsboard:master Nov 9, 2026
2 checks passed
@vvlladd28
vvlladd28 deleted the feature/openapi-compat-layer branch November 9, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants