Skip to content

fix(case): stop Snakeize treating digits as uppercase letters - #2015

Closed
zunixport wants to merge 1 commit into
ProjectOpenSea:mainfrom
zunixport:fix/snakeize-type-level-digit-underscore
Closed

zunixport wants to merge 1 commit into
ProjectOpenSea:mainfrom
zunixport:fix/snakeize-type-level-digit-underscore

Conversation

@zunixport

Copy link
Copy Markdown
Contributor

Snakeize puts a spurious underscore before every digit

camelToSnake matches /[A-Z]/, but the type-level mapper decided "is this an uppercase letter?" with Head extends Uppercase<Head>. Uppercase is the identity on digits and on anything without a case, so "1" extends Uppercase<"1"> is true and every digit read as an uppercase letter.

key type said runtime wrote
chain2Id chain_2_id chain2_id
erc721TokenId erc_7_2_1_token_id erc721_token_id

A leading underscore is affected by the same cause (Uppercase<"_"> is "_"): Snakeize<{ _private: number }> resolved to __private while the runtime leaves _private alone.

Why it matters

Snakeize<T> is exported from the package root and describes outgoing request bodies, so this is the request-side counterpart of the response-casing mismatch documented in AGENTS.md rule 9 — and the compiler cannot catch it:

  • snakeizeKeysDeep({ chain2Id: x }) is typed as { chain_2_id: string }
  • the reader writes body.chain_2_id
  • the type and the reader agree with each other, so tsc is satisfied
  • at runtime the key is chain2_id, so the read is undefined

A value that type-checks and comes back undefined is the worst of the two failure modes; the type is supposed to describe the value the caller actually gets.

Fix

Compare against Lowercase instead. That is true for exactly the characters Lowercase rewrites, which is the set the runtime /[A-Z]/ matches. The existing cases are unchanged: MyKey is still my_key, URL is still u_r_l.

Tests

Extended the existing Snakeize<T> block in test/utils/case.spec.ts with the digit case (a type assertion plus a runtime assertion, which is what vitest actually runs) and the leading-underscore case. The type assertions are read by check-types, as the file's other type tests already note.

@ryanio

ryanio commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

Thanks @zunixport, the digit mismatch is real. This repo is a read-only mirror, so we've recreated the fix in our internal monorepo with you credited as co-author. It ships in the upcoming @opensea/sdk 12.11.1.

One correction: the old type already mapped _private to _private, because position 0 never gets an underscore, so that test passed without the fix. The underscore bug is mid-key instead: an already snake_case key like order_hash came out as order__hash. We tested that case in its place.

Closing this PR since the change now lives in the monorepo. Appreciate it!

@ryanio ryanio closed this Sep 30, 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