Skip to content

feat: add Advisors, Placeholders and Organization APIs; harden asset translation detection - #434

Merged
andrii-bodnar merged 3 commits into
masterfrom
feature/advisors-placeholders-organization
Nov 4, 2026
Merged

andrii-bodnar merged 3 commits into
masterfrom
feature/advisors-placeholders-organization

Conversation

@katerina20

@katerina20 katerina20 commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds the Advisors, Placeholders and Organization APIs from the current Crowdin OpenAPI specifications (crowdin + enterprise, file-based + string-based). It also hardens the asset-translation classifier added in #433. All changes are additive and keep source and binary compatibility with the latest release, 1.37.0.

Coverage: 22 of the 23 non-deprecated Advisors / Placeholders / Organization routes. It is not complete: GET /external-qa-checks/{externalQaCheckId} is the one remaining gap (see below).

The PR has three commits:

  1. fix(translations): the classifier hardening, limited to the deserializer and its tests.
  2. feat: the three new APIs, wired into Client as getAdvisorsApi(), getPlaceholdersApi() and getOrganizationApi().
  3. fix(organization): isVendor / isVendorRaw setters kept in sync.

New API methods

Advisors (AdvisorsApi, all 4 editions)

Method Endpoint
runAdvisorCheck(projectId, RunAdvisorCheckRequest) POST /projects/{projectId}/advisors/checks
getAdvisorCheck(projectId, String checkId) GET /projects/{projectId}/advisors/checks/{checkId}
listAdvisorInsights(projectId, ListAdvisorInsightsParams) GET /projects/{projectId}/advisors/insights
editAdvisorInsight(projectId, insightId, List<PatchRequest>) PATCH /projects/{projectId}/advisors/insights/{insightId}
upsertApplicationAdvisorInsight(projectId, applicationIdentifier, moduleKey, UpsertAdvisorInsightRequest) PUT /projects/{projectId}/applications/{applicationIdentifier}/modules/{moduleKey}/advisors/insights

upsertApplicationAdvisorInsight returns void, because both success responses (200 updated, 201 created) have no body.

Placeholders (PlaceholdersApi)

Method Endpoint Editions
listSystemPlaceholders(projectId, limit, offset) GET /projects/{projectId}/system-placeholders all 4
editSystemPlaceholders(projectId, List<PatchRequest>) PATCH /projects/{projectId}/system-placeholders all 4
listCustomPlaceholders(limit, offset) GET /custom-placeholders Enterprise
addCustomPlaceholder(AddOrganizationPlaceholderRequest) POST /custom-placeholders Enterprise
getCustomPlaceholder(id) / editCustomPlaceholder(id, patch) / deleteCustomPlaceholder(id) `GET PATCH
listProjectPlaceholders(projectId, limit, offset) GET /projects/{projectId}/placeholders Enterprise
addProjectPlaceholder(projectId, AddProjectPlaceholderRequest) POST /projects/{projectId}/placeholders Enterprise
getProjectPlaceholder / editProjectPlaceholder / deleteProjectPlaceholder `GET PATCH

System, organization custom and project placeholders are separate models (SystemPlaceholder, OrganizationPlaceholder, ProjectPlaceholder). The deprecated AI custom-placeholder endpoints (/ai/settings/custom-placeholders, /users/{userId}/ai/settings/custom-placeholders) are already in the released AIApi. They are not duplicated or extended here.

Organization (OrganizationApi, Enterprise)

Method Endpoint
getOrganization() GET /organization
getOrganizationAuthSettings() GET /organization/auth-settings
listCustomSpellcheckers(limit, offset) GET /custom-spellcheckers
getCustomSpellchecker(id) GET /custom-spellcheckers/{id}
listExternalQaChecks(projectId, limit, offset) GET /external-qa-checks

Intentionally omitted: GET /external-qa-checks/{externalQaCheckId} (approved; the one remaining coverage gap)

The spec ("Get External QA Check", api.external-qa-checks.get) documents the response as ExternalQaCheckCollectionResource, the same schema as the list endpoint. data is an array, even though the URL addresses one resource. Both Enterprise editions say the same, and there is no response example to settle it. Returning a single object would contradict the documented contract, and returning a list for a single-resource GET would likely be wrong. So no method or model is added. Maintainer decision (approved): keep this endpoint unimplemented until Crowdin confirms the response shape.

To implement it later, Crowdin needs to confirm whether the live response is {"data": {…}} (single object) or {"data": [{"data": {…}}]} (collection), ideally with a real response example in the docs. With a single object, the method would be ResponseObject<ExternalQaCheck> getExternalQaCheck(Long externalQaCheckId), reusing the existing ExternalQaCheckResponseObject.

Organization.isVendor decision

Evidence: both Enterprise specs declare isVendor as {"type": "string", "example": false}. The type is string, but the schema's own example is a boolean, and GET /organization has no response example.

Implementation: the SDK accepts both shapes without guessing or losing data.

  • Boolean isVendor is set from a JSON boolean, or from the strings "true" / "false" (case-insensitive). Any other value leaves it null; nothing is converted from "1", "yes" and the like.
  • String isVendorRaw always keeps the raw value as text, e.g. "false" or "vendor", so an unexpected string is never lost.
  • Unexpected values never make the whole response fail.

This follows the dual-field pattern of excludedElements in #430. Tests cover JSON booleans, boolean strings, unrecognized strings, numbers, null, missing, and serialization.

Maintainer decision (approved): keep both isVendor and isVendorRaw for now, because the specification contradicts its own example. If Crowdin confirms the live API returns a boolean, isVendorRaw simply stays redundant.

Setters (resolved). Both setters are now explicit and keep the two fields in sync. Their signatures are identical to the Lombok-generated ones.

  • setIsVendor(Boolean) sets the raw value to "true" / "false".
  • setIsVendorRaw(String) keeps the string exactly as given (e.g. "TRUE", "vendor") and recomputes isVendor with the same rule as the JSON reader. Unknown strings give null.
  • null passed to either setter clears both fields.
  • The JSON reader now goes through the same setters, so values set in code and values read from JSON always agree.

Remaining limitation: serialization normalizes the shape. "true" / "false" strings are written back as JSON booleans, and unexpected non-string values (numbers, objects) are written back as strings. The meaning is preserved and there is always exactly one isVendor key.

Asset translation classifier hardening (follow-up to #433)

An unknown List Language Translations contentType is now read as AssetLanguageTranslations only when all of these hold:

  • contentType is a non-empty string. null, "", whitespace, numbers and booleans are now rejected; objects and arrays were already rejected.
  • It is not one of Crowdin's own application/vnd.crowdin.* text types.
  • The documented asset shape is present: a url, without text or plurals.

Unchanged:

  • text/plain, plural and ICU translations map exactly as before.
  • Every other unknown shape is still rejected with Wrong type: …, and a missing contentType still throws Couldn't get class.

Remaining ambiguity (documented by a test): text/markdown with a url and no text is still read as an asset. text/* is not excluded, because the API doesn't state that an asset can never have a text/* MIME type.

Validation

  • Tests: full suite on JDK 8: 870 passed, 0 failed. New or affected classes:
    • AdvisorsApiTest (5), PlaceholdersApiTest (12), OrganizationApiTest (6), OrganizationIsVendorTest (10, including setter call orders, sync, and setter/JSON-reader agreement).
    • LanguageTranslationsDeserializationTest (11, of which 6 are new classifier regression tests).
    • StringTranslationsApiTest (44, unchanged).
    • API tests check the exact URL, method, query and request body.
  • Builds: ./gradlew clean build passes on JDK 17 and JDK 8. JDK 8 Javadoc: 0 errors.
  • OpenAPI coverage (all 4 editions): 22 of the 23 non-deprecated Group 11 routes are implemented, with matching methods, paths, query params and response kinds. The 23rd is the omitted GET /external-qa-checks/{id}. Path parameter types match the spec: integers as Long; checkId, applicationIdentifier and moduleKey as String.
  • Recursive model check: no missing fields, no SDK-only fields, no type mismatches and no new deprecated fields. The only flag is Organization.isVendor, which the script can't see because it's read via @JsonSetter; it's covered by OrganizationIsVendorTest.
  • JSON: no duplicate JSON property names across 41 new model classes; Jackson builds a serializer and deserializer for each.
  • Compatibility with the published 1.37.0 jar: 0 removed or changed public signatures, and no new overloads that are ambiguous with null arguments.

Known limitations and unverified contracts (spec only, no live check)

  • GET /external-qa-checks/{id} is omitted (see above).
  • Organization.isVendor: the live type is unconfirmed (see above).
  • Asset classifier: text/* assets can't be told apart from unknown text types that have a url and no text.
  • Advisors and the new Placeholders/Organization endpoints are modelled from the spec only, not tested against the live API.

Maintainer decisions

  1. Approved: keep GET /external-qa-checks/{id} unimplemented until Crowdin confirms whether the response is a single object or a collection. This is the one remaining coverage gap (22/23).
  2. Approved: keep both Organization.isVendor and Organization.isVendorRaw for now.

Optional follow-up: setters that keep isVendor and isVendorRaw in sync. Resolved in fix(organization).

🤖 Generated with Claude Code

katerina20 and others added 3 commits November 3, 2026 20:00
…translations

Unknown language translation types are read as assets only when contentType is a
non-empty string outside Crowdin's application/vnd.crowdin.* text types and the
item has a url without text or plurals. Other unknown shapes stay rejected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Advisors: run and get advisor checks, list and edit insights, upsert an
  application module insight
- Placeholders: system placeholders (list, batch edit), organization custom
  placeholders and project placeholders (list, add, get, edit, delete)
- Organization: get organization and auth settings, custom spellcheckers,
  external QA checks list
- Organization.isVendor accepts a boolean or a string and keeps the raw value
- GET /external-qa-checks/{id} is not added: the spec documents a collection
  response for a single resource

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
setIsVendor also sets the raw value ("true" / "false"), setIsVendorRaw keeps
the raw string and recomputes the boolean with the same rules as the JSON
reader, and null clears both fields. Signatures and JSON output are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@andrii-bodnar
andrii-bodnar merged commit 8ca9796 into master Nov 4, 2026
3 checks passed
@andrii-bodnar
andrii-bodnar deleted the feature/advisors-placeholders-organization branch November 4, 2026 07:31
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