Skip to content

fix(getTraits): stop camelizing collection-authored trait keys - #1989

Closed
crazywriter1 wants to merge 1 commit into
ProjectOpenSea:mainfrom
crazywriter1:fix/traits-response-camelize
Closed

fix(getTraits): stop camelizing collection-authored trait keys#1989
crazywriter1 wants to merge 1 commit into
ProjectOpenSea:mainfrom
crazywriter1:fix/traits-response-camelize

Conversation

@crazywriter1

Copy link
Copy Markdown

getTraits returns objects keyed by trait names and trait values, but Fetcher camelizes every response body. A collection with a fur_color trait gets reported as furColor, and its dark_brown value as darkBrown.

Given this response:

{
  "categories": { "fur_color": "string" },
  "counts": { "fur_color": { "dark_brown": 12, "darkBrown": 3 } }
}

the SDK currently returns counts.furColor as { "darkBrown": 3 }. The rename alone breaks any lookup by trait name, but the two distinct values also collide into a single key — one trait silently disappears and the surviving count is wrong. Nothing in the response tells the caller this happened.

Why this is a v11 regression

Response camelization is applied to every endpoint by e7deba3 in 11.0.0:

Rebuild the SDK's type layer on @opensea/api-types with automatic case translation at the fetcher boundary. […] the fetcher snakeizes outgoing query params and POST bodies and camelizes responses, so the SDK no longer ships hand-rolled response shapes.

That assumption — response keys are field names, so rewriting them is safe — holds for every spec-derived response. getTraits predates the change and is the one endpoint it doesn't hold for: the OpenAPI spec has no schema for /api/v2/traits/{slug}, only an example, whose keys are the collection's own trait names (face, background, level). Because there was no generated type to migrate to, GetTraitsResponse stayed hand-written in src/api/types.ts as a pair of index signatures — while the blanket camelizer was applied to it along with everything else.

Before v11 there was no camelization in src/api/api.ts at all; conversion was per-endpoint in utils/converters.ts, which this endpoint never opted into. So this looks like collateral damage of the migration rather than an intentional choice.

Why the tests didn't catch it

Every existing getTraits test mocks the fetcher (mockGet.mockResolvedValue(...)), so the camelization step never runs in the suite. The fixtures also happen to use casing-free names like Background and Golden Brown. One of them is already affected in principle — "Special-Character_123" would come back as "Special-Character123" through the real pipeline — but the mock means it never gets there.

The fix

Adds camelizeResponse to RequestOptions, mirroring the existing snakeizeBody opt-out for request bodies, and sets it to false for getTraits:

const response = await this.fetcher.get<GetTraitsResponse>(
  path,
  undefined,
  { camelizeResponse: false },
)

Both get and request honour it, so the option doesn't silently do nothing on writes. Opting out is complete for this endpoint rather than a partial workaround: the response's own field names (categories, counts, and min/max for numeric traits) are single words with nothing to convert, so everything left is data that must survive verbatim. GetTraitsResponse is unaffected at the type level, since Camelize<T> passes index signatures through unchanged.

Scope is one endpoint — GetTraitsResponse is the only response type in src/api/types.ts keyed by data rather than by field names.

Tests

  • End-to-end test in test/api/api.spec.ts that stubs fetch and asserts both trait names and trait values survive, including the two values that otherwise collide.
  • Confirmed it is a real tripwire: with the fix reverted it fails with expected { furColor: 'string' } to deeply equal { fur_color: 'string' }.
  • Call-site test in test/api/collections.spec.ts asserting the opt-out is passed, so a future refactor of getTraits can't quietly drop it.
  • Full suite green (794 tests), npm run check-types and Biome clean.

getTraits returns objects keyed by trait names and trait values, but Fetcher camelizes every response body, so a collection with a fur_color trait is reported as furColor and a dark_brown value as darkBrown. Two values differing only in casing collapse into one entry with the wrong count.

This is a v11 regression: getTraits landed in v10 (ProjectOpenSea#1787), and the blanket camelizeKeysDeep in api.ts arrived in v11.0.0 (e7deba3), replacing per-endpoint conversion in utils/converters.ts. The blanket pass assumes response keys are field names, which holds everywhere except this endpoint.

Adds RequestOptions.camelizeResponse, mirroring the existing snakeizeBody opt-out, and sets it to false for getTraits. The response's own field names (categories, counts) are single words, so nothing is left unconverted.
@ryanio

ryanio commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Thanks for this! We recreated the fix in our internal monorepo, with you credited as co-author on the commit: getTraits now opts out of response camelization, so trait names and values come back as the collection authored them, and values differing only by case are no longer merged.

This repo is a read-only mirror, so we can't merge PRs here directly, but we read every one. This will ship in the next @opensea/sdk release. Note that we've also renamed this repo from opensea-js to opensea-sdk. Appreciate you taking the time.

@ryanio ryanio closed this Aug 14, 2026
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