Skip to content

feat!: Introduce @seamapi/url-search-params-parser behind useLegacyQueryParamsParser - #182

Draft
razor-x wants to merge 1 commit into
betafrom
claude/url-param-parser-migration-i9foa1
Draft

feat!: Introduce @seamapi/url-search-params-parser behind useLegacyQueryParamsParser#182
razor-x wants to merge 1 commit into
betafrom
claude/url-param-parser-migration-i9foa1

Conversation

@razor-x

@razor-x razor-x commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

Targets beta (v4). Introduces query string parsing with @seamapi/url-search-params-parser (in generous mode, strict: false) behind a new useLegacyQueryParamsParser option — with the legacy parser remaining the default.

The release plan:

  • v4 (this PR, on beta): dropping CJS/ESM-only is the headline breaking change (already on beta via 4.0.0-beta.1). The new parser ships behind an option; upgrading changes no query parsing behavior by default.
  • v5: the default swaps to the new parser; useLegacyQueryParamsParser sticks around as the escape hatch before being removed in a later release.

The option

Settable in two places; the route spec overrides the factory:

// Opt a whole factory into the new parser:
export const withRouteSpec = createWithRouteSpec({ ..., useLegacyQueryParamsParser: false })

// Or migrate route by route on a shared factory (e.g. seam-connect):
export const route_spec = checkRouteSpec({
  methods: ["GET"],
  useLegacyQueryParamsParser: false, // this route uses the new parser
  queryParams,
  ...
})

The parser module is loaded lazily (dynamic import()), so routes on the legacy default never load it, and consumers loading nextlove through a CommonJS transform (e.g. tsx/cjs, esbuild-register test setups) don't need to resolve the ESM-only dependency unless they opt in.

Breaking change (beyond the ESM-only change already on beta)

  • supportedArrayFormats is removed from SetupParams/RequestInput, along with the QueryArrayFormat/QueryArrayFormats types and the DEFAULT_ARRAY_FORMATS export. All callers were using the default (all three formats), and both parsers now accept the repeated, bracket, and comma formats on every route. Routes that restricted formats lose the Bracket syntax not supported... / Repeated parameters not supported... 400s.

What changes when a route opts in (useLegacyQueryParamsParser: false)

None of this applies on the default; it's the v5 migration checklist.

New capabilities (previously rejected, now accepted):

  1. z.number() / z.date() query params parse without z.coerce (?limit=55); whitespace is trimmed around numbers/booleans/dates.
  2. Generous booleans: true/True/TRUE/yes/Yes/YES/1true; false/False/FALSE/no/No/NO/0false (legacy: only "true" is truthy, anything else is false).
  3. Nested objects and records via dot notation: ?address.city=SF, ?tags.a=1&tags.b=2.
  4. Top-level and property-level z.union() / z.discriminatedUnion() of objects are parsed.

Stricter parsing (previously accepted, now 400):

  1. Junk boolean values fail (?flag=yolo was silently false, now 400 invalid_input).
  2. Empty string params are null (?name= passed z.string() as "", now fails unless nullable; omitting the param still works for optional fields).
  3. Ambiguous input → 400 invalid_query_params: repeated non-array params (?id=1&id=2), mixed array formats (?ids=1&ids[]=2), commas inside bracket values (?ids[]=a,b, previously split), repeated values containing commas (?ids=a,b&ids=c,d, previously literal), arrays mixing empty and non-empty values. Net: array values containing a literal comma can't be transmitted in generous mode.
  4. commonParams body values are no longer coerced ({"ids": "a,b"} was comma-split, {"flag": "true"} coerced) — bodies validate as-is.
  5. Boolean dynamic route params stay raw strings (legacy coerced them).

Schema restrictions (route authors): unsupported constructs fail every request with 500 unparseable_schema instead of validating raw strings — z.array(z.boolean()), arrays of objects, nullable arrays, z.tuple() (see #180), z.any()/z.unknown()/z.bigint() props, intersections/maps/sets, records of non-primitives, unions of incompatible primitives. Inventory route schemas before opting in (or opt in per route and keep the offenders on legacy until adjusted). Error type changes: ambiguity errors move from Zod's invalid_input to invalid_query_params.

Implementation notes

  • New-parser path: parse req.url's search params (strict: false), strip absent (undefined) keys, overlay Next.js dynamic route params and unknown params (preserving .strict()/.passthrough() semantics), then schema.parse. UnparseableSearchParamError400 invalid_query_params; UnparseableSchemaError500 unparseable_schema.
  • Legacy path is byte-for-byte the previous coercion, with all array formats enabled (the removed option's default).
  • commonParams merges the raw (pre-validation) body over the query in both paths.

Testing

  • Root package: typecheck + 8 ava tests pass.
  • Example app: 42/42 integration tests pass against the published parser 0.2.1, including:
    • legacy default coverage (no option set): all three array formats, "yolo"false, repeated values keeping commas
    • new-parser factory coverage: all formats, mixed-format 400, junk-boolean 400, generous booleans
    • per-route override on a legacy-default factory (the seam-connect migration path)

@razor-x razor-x changed the title feat: Replace hand-rolled query param parsing with @seamapi/url-search-params-parser feat!: Replace hand-rolled query param parsing with @seamapi/url-search-params-parser Aug 5, 2026
…eryParamsParser

Add query string parsing with @seamapi/url-search-params-parser
(in generous mode) behind a new useLegacyQueryParamsParser option,
settable on createWithRouteSpec and per route via the route spec.

The legacy hand-rolled coercion remains the default in v4, so upgrading
changes no query parsing behavior; set useLegacyQueryParamsParser:
false to opt into the new parser. The default will swap in v5.

The parser module is loaded lazily, so routes on the legacy default
never load it and CommonJS-transformed consumers do not need to
resolve the ESM-only dependency.

BREAKING CHANGE: The supportedArrayFormats setup option, the
QueryArrayFormat and QueryArrayFormats types, and the
DEFAULT_ARRAY_FORMATS export are removed. All routes now accept the
repeated, bracket, and comma array formats, which was already the
default behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jWzL9LC7Q6bTGaXDoKq4z
@razor-x
razor-x force-pushed the claude/url-param-parser-migration-i9foa1 branch from b52c8fc to d55e304 Compare August 6, 2026 07:07
@razor-x razor-x changed the title feat!: Replace hand-rolled query param parsing with @seamapi/url-search-params-parser feat!: Introduce @seamapi/url-search-params-parser behind useLegacyQueryParamsParser Aug 6, 2026
@razor-x
razor-x changed the base branch from main to beta August 6, 2026 07:07
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