Skip to content

bug(angular): required+nullable query params are silently dropped without a paramsSerializer #3712

Description

@the-ult

Summary

For Angular HttpClient services and httpResource functions, a query parameter that is both required: true and nullable per the OpenAPI schema is silently dropped from the request whenever its runtime value is null — unless a custom paramsSerializer mutator is configured. This can produce a request the server doesn't recognize as complete.

Reproduction (on current master)

samples/angular-app/src/api/http-client/pets/pets.service.ts (generated from tests/specifications/petstore.yaml's searchPets operation, whose SearchPetsParams declares requirednullableString/requirednullableStringTwo as required + nullable):

searchPets<TData = Pets>(
  params: SearchPetsParams,
  version: number = 1,
  options?: HttpClientObserveOptions,
): Observable<TData | HttpEvent<TData> | AngularHttpResponse<TData>> {
  const filteredParams = filterParams(
    { ...params, ...options?.params },
    new Set<string>(['requirednullableString', 'requirednullableStringTwo']),
  );
  ...

filterParams is called with only 2 arguments, so preserveRequiredNullables defaults to false. When params.requirednullableString === null, the key is dropped from filteredParams entirely — the request goes out without it, even though the parameter is documented as required.

Root cause

preserveRequiredNullables is only ever set to true when a paramsSerializer mutator is configured:

  • packages/core/src/generators/options.ts, generateAxiosOptions — all three isAngular param-building branches (!isRequestOptions IIFE, and the isRequestOptions angularParamsRef/paramsSerializer/plain branches) gate on !!paramsSerializer or unconditional true only inside the paramsSerializer branch; the plain (no-serializer) branch never passes it.
  • packages/angular/src/http-resource.ts:491 — same gating (preserveRequiredNullables: !!paramsSerializer), so httpResource functions have the identical gap.

This is not a regression — it has been the design since the feature was introduced in 6672367 (feat(angular): preserve required nullable query params). The reason: Angular's own HttpClient/httpResource params option type (@angular/common/http, e.g. RequestOptions.params and HttpResourceRequest.params) is:

params?: HttpParams | Record<string, string | number | boolean | ReadonlyArray<string | number | boolean>>;

— it structurally excludes null. Preserving a literal null in the filtered params object is only type-safe when a custom paramsSerializer consumes and converts it before the result reaches Angular's typed params: field; without a serializer, the value has nowhere valid to go.

Note for reviewers: this means the naive fix of adding true as a 3rd argument to the filterParams(...) call in the plain path (as suggested by an automated review comment on #3707) does not actually work — filterParams(..., true) returns Record<string, AngularHttpParamValueWithNullable> (includes null), which is not assignable to Angular's params: field and would fail to compile.

Proposed fix

Don't try to preserve the literal null on the no-serializer path — instead, when a required-nullable key's value is null, emit an empty string ('') instead of dropping the key. This:

  • needs no type widening ('' is already a valid AngularHttpParamValue, no | null needed, no overload split for this case);
  • is accepted by Angular's HttpParams natively, encoding as ?key= in the request;
  • preserves the key's presence (closer to the "required" contract) instead of silently omitting it, without misrepresenting null as some other value.

Concretely, in packages/core/src/generators/options.ts's getAngularFilteredParamsHelperBody() (and the non-shared-helper IIFE twin, getAngularFilteredParamsExpression), add an unconditional branch (no preserveRequiredNullables gate needed, since '' requires no type change):

} else if (value === null && requiredNullableParamKeys.has(key)) {
  filteredParams[key] = '';
}

placed before the existing preserveRequiredNullables-gated null-preserving branch, so:

  • no paramsSerializer → required-nullable null becomes '' (key preserved, always-on, no config needed);
  • paramsSerializer configured (preserveRequiredNullables: true) → unchanged, literal null still passed through to the serializer as today, since the serializer explicitly opted into richer null-handling.

This should also thread through packages/angular/src/http-resource.ts for consistency, since it hits the identical gap.

Open question for maintainer input: whether '' is the right universal default, or whether this should be configurable (e.g. an opt-out for users who'd prefer the current drop-silently behavior) — flagging as a design decision rather than presupposing the answer.

Affected areas

  • packages/core/src/generators/options.ts (getAngularFilteredParamsHelperBody, getAngularFilteredParamsExpression, generateAxiosOptions)
  • packages/angular/src/http-client.ts (consumer)
  • packages/angular/src/http-resource.ts:491 (consumer, same gap)

Related issues

Feature history for preserveRequiredNullables/requiredNullableParamKeys: 6672367, e2d3c2e, 4acceae (original feature), 7b80844 (#3168, the type-safety fix above), 5440a4c (#3597, the TS6133 fix above).

Surfaced via automated review on #3707 (output.artifacts, unrelated feature — output.artifacts only emits barrels from already-generated files and cannot fix generator-level behavior).

Metadata

Metadata

Assignees

Labels

angularRelated to Angular generation issues

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions