# Angular

Generate fully typed Angular services using `HttpClient` or signal-first
`httpResource` functions from your OpenAPI specification.

If you want the short version:

- use `httpClient` for classic Angular service-based APIs
- use `httpResource` for signal-first read flows
- use `both` when you want resource-based reads and service-based writes together
- enable `override.angular.runtimeValidation` when you generate Zod schemas and
  want runtime response validation

## Configuration

Set `output.client` to `angular` to enable Orval's Angular generator:

```ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      mode: 'tags-split',
      target: 'src/api/petstore.ts',
      schemas: 'src/api/model',
      client: 'angular',
      mock: true,
    },
    input: {
      target: './petstore.yaml',
    },
  },
});
```

`output.client = 'angular'` selects the Angular generator. After that, use
`override.angular.retrievalClient` to choose how **retrieval-style** Angular
operations should be generated. The older `override.angular.client` key remains
supported as a backward-compatible alias.

## Generated Output

By default, the Angular client generates injectable service classes backed by
`HttpClient`.

Available Angular retrieval modes:

- `httpClient` — keep retrievals as injectable service methods backed by `HttpClient`
- `httpResource` — generate signal-first `httpResource` functions for
  retrieval-style operations
- `both` — generate `httpResource` retrievals and keep `HttpClient` methods for
  imperative request handling

Mutation-style operations such as create, update, delete, and most imperative
`POST` calls continue to use generated `HttpClient` service methods by default.
If you need different behavior for a specific operation, use an operation-level
Angular override.

Register the generated service via DI (standalone or module-based apps both
work):

```ts
import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig } from '@angular/core';

import { PetstoreService } from './api/petstore';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient(), PetstoreService],
};
```

`providedIn` applies to generated service classes only. `httpResource`
functions are plain exports, so you import and call them directly where you
need them.

## Choosing the Angular output mode

Use `override.angular.retrievalClient` to choose the generated Angular
retrieval mode:

```ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      mode: 'tags-split',
      target: 'src/api/petstore.ts',
      schemas: 'src/api/model',
      client: 'angular',
      override: {
        angular: {
          retrievalClient: 'httpClient',
        },
      },
    },
    input: {
      target: './petstore.yaml',
    },
  },
});
```

If you omit `override.angular.retrievalClient` (or the legacy
`override.angular.client` alias), Orval uses the default Angular
`HttpClient` service-class output.

### Which mode should you choose?

| `override.angular.retrievalClient` | What Orval generates | Choose it when |
|---|---|---|
| `httpClient` | Retrievals stay on injectable service classes backed by `HttpClient` | You want conventional Angular services, imperative request methods, and the broadest compatibility with request options and mutators |
| `httpResource` | Retrieval-style operations become signal-first `httpResource` functions; mutations still use `HttpClient` service methods | You want Angular-native signal ergonomics for reads and prefer resource-based data fetching |
| `both` | Service classes plus sibling `*.resource.ts` retrieval helpers | You want signal-first reads and imperative writes together |

Quick rule of thumb:

- choose `httpClient` for classic Angular service-based APIs
- choose `httpResource` for signal-first read flows
- choose `both` when your app wants both patterns side-by-side
- if you do not set `override.angular.retrievalClient`, Orval defaults to `httpClient`
- create, update, delete, and other mutation-style operations still use `HttpClient` methods by default

## Setting the Backend URL

### Single API

Use an HTTP interceptor to automatically add the API base URL. In modern
standalone Angular apps, a functional interceptor keeps the setup compact:

```ts
import { HttpInterceptorFn } from '@angular/common/http';

export const apiInterceptor: HttpInterceptorFn = (req, next) => {
  return next(
    req.clone({
      url: `https://api.example.com${req.url}`,
    }),
  );
};
```

```ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { ApplicationConfig } from '@angular/core';

import { apiInterceptor } from './api.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient(withInterceptors([apiInterceptor]))],
};
```

An interceptor is global, though: it can only route by sniffing the outgoing
`req.url`. That falls apart once you generate more than one Angular API into
the same app and sit them behind a gateway or proxy that assigns each API its
own path prefix (or host) — the interceptor has no reliable way to know which
generated client a given request came from.

### DI-based base URL composition (multiple APIs / gateway routing)

Set `override.angular.baseUrl` to compose the base URL for a specific output
through Angular's dependency injection instead of a global interceptor. Unlike
the interceptor, the base URL is resolved per generated API, so a gateway that
maps different generated clients to different upstream paths can be modeled
directly in DI.

`apiId` is required and explicit — Orval never derives it from the
specification title or file name — so the generated token and helper names
stay stable across regenerations:

```ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      mode: 'tags-split',
      target: 'src/api/petstore.ts',
      schemas: 'src/api/model',
      client: 'angular',
      override: {
        angular: {
          baseUrl: { apiId: 'petstore' },
        },
      },
    },
    input: {
      target: './petstore.yaml',
    },
  },
});
```

#### Generated artifacts

With `override.angular.baseUrl` set, Orval emits a sibling `<target>.base-url.ts`
file alongside the generated client:

```ts
import { InjectionToken, inject, type Provider } from '@angular/core';

export const PETSTORE_SERVER_URL: string = 'http://petstore.swagger.io/v1';

export function normalizeBaseUrl(baseUrl: string): string {
  return baseUrl.replace(/\/+$/, '');
}

export interface PetstoreBaseUrlResolverContext {
  readonly apiId: 'petstore';
  readonly serverUrl: string;
}

export type PetstoreBaseUrlResolver = (
  context: PetstoreBaseUrlResolverContext,
) => string;

export const PETSTORE_BASE_URL_RESOLVER =
  new InjectionToken<PetstoreBaseUrlResolver>('PETSTORE_BASE_URL_RESOLVER', {
    providedIn: 'root',
    factory: (): PetstoreBaseUrlResolver => (context) => context.serverUrl,
  });

export const PETSTORE_BASE_URL = new InjectionToken<string>(
  'PETSTORE_BASE_URL',
  {
    providedIn: 'root',
    factory: (): string => {
      const resolver = inject(PETSTORE_BASE_URL_RESOLVER);
      return normalizeBaseUrl(
        resolver({ apiId: 'petstore', serverUrl: PETSTORE_SERVER_URL }),
      );
    },
  },
);

export function providePetstoreBaseUrl(baseUrl: string): Provider {
  return { provide: PETSTORE_BASE_URL, useValue: normalizeBaseUrl(baseUrl) };
}

export function providePetstoreBaseUrlResolver(
  resolver: PetstoreBaseUrlResolver,
): Provider {
  return { provide: PETSTORE_BASE_URL_RESOLVER, useValue: resolver };
}
```

Identifiers are derived from `apiId` alone (`petstore` → `PETSTORE_*` /
`Petstore*` / `providePetstore*`), so they stay collision-free when multiple
outputs with different `apiId`s are generated into the same app. Every
generated `HttpClient` service method and `httpResource` function in this
output injects `PETSTORE_BASE_URL` and prefixes its route with it.

#### Precedence

`PETSTORE_BASE_URL` resolves in this order:

1. A directly provided value via `providePetstoreBaseUrl(...)` — wins outright,
   the resolver below is never invoked.
2. A resolver provided via `providePetstoreBaseUrlResolver(...)`.
3. The default resolver factory, which just returns the embedded server URL.
4. The embedded `PETSTORE_SERVER_URL` constant, resolved at generation time
   from the specification's `servers` field (empty string `''` when the
   specification has no `servers` entry, which yields relative URLs).

Whatever value wins is passed through `normalizeBaseUrl`, which strips
trailing slashes (`'/api/x/'` → `'/api/x'`, `'https://h/'` → `'https://h'`,
`'/'` → `''`). Generated routes always start with `/`, so plain interpolation
(`` `${baseUrl}${route}` ``) can never double up or drop the separator between
them.

#### Multiple APIs behind one gateway

Because `InjectionToken` identity is per output, two generated outputs can't
literally share one token instance without a shared runtime package — Orval's
Angular output has no runtime dependency, by design. Instead, share one
resolver *function* and register it against each output's resolver token.
Type it against a small structural interface so it's assignable to every
generated `<Api>BaseUrlResolver`, regardless of `apiId`:

```ts
import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig } from '@angular/core';

import { providePetstoreBaseUrlResolver } from './api/petstore.base-url';
import { provideInventoryBaseUrlResolver } from './api/inventory.base-url';

interface GatewayContext {
  apiId: string;
  serverUrl: string;
}

// One registry, one resolver function, shared across every generated API.
const gatewayRoutes: Record<string, string> = {
  petstore: '/gateway/petstore',
  inventory: '/gateway/inventory',
};

const gatewayResolver = (ctx: GatewayContext): string =>
  gatewayRoutes[ctx.apiId] ?? ctx.serverUrl;

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    providePetstoreBaseUrlResolver(gatewayResolver),
    provideInventoryBaseUrlResolver(gatewayResolver),
  ],
};
```

Each output still resolves its *own* token independently — `gatewayResolver`
is just dispatched with a different `apiId` depending on which token invoked
it — so requests from the petstore client and the inventory client can land on
different upstream paths (or hosts) through the same gateway.

#### Testing

Override the token (or the resolver) in `TestBed` like any other provider:

```ts
import { provideHttpClient } from '@angular/common/http';
import {
  HttpTestingController,
  provideHttpClientTesting,
} from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';

import {
  providePetstoreBaseUrl,
  providePetstoreBaseUrlResolver,
} from './api/petstore.base-url';
import { PetsService } from './api/pets/pets.service';

describe('PetsService', () => {
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        provideHttpClient(),
        provideHttpClientTesting(),
        providePetstoreBaseUrl('/gateway/petstore'),
        // or: providePetstoreBaseUrlResolver((ctx) => `/gateway/${ctx.apiId}`),
      ],
    });

    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => httpMock.verify());

  it('prefixes requests with the provided base URL', () => {
    TestBed.inject(PetsService)
      .createPets({ name: 'Rex', tag: 'dog' })
      .subscribe();

    httpMock.expectOne('/gateway/petstore/v1/pets').flush(null);
  });
});
```

The same token, provided once in `TestBed`, backs both the `HttpClient`
service and any `httpResource` functions generated for the same output — so a
single provider override is enough to redirect every request in a test.

#### `httpResource` and injection context

Generated `httpResource` functions read the token with `inject()` when called
inside an injection context, and fall back to `options.injector.get(...)` when
an explicit `injector` is passed — the same rule that already applies to every
other injected dependency in generated `httpResource` functions:

```ts
export function showPetByIdResource(
  petId: Signal<string>,
  accept?: ShowPetByIdAccept,
  version?: Signal<number>,
  options?: OrvalHttpResourceOptions<Pet, unknown>,
): HttpResourceRef<Pet | undefined> {
  const baseUrl = options?.injector
    ? options.injector.get(PETSTORE_BASE_URL)
    : inject(PETSTORE_BASE_URL);
  // ...
}
```

Call these functions during Angular's injection context (a constructor, a
field initializer, or `runInInjectionContext`), or pass an explicit
`injector` in `options` when you can't.

#### Notes

- **Zod runtime validation is unaffected.** `override.angular.runtimeValidation`
  keeps validating responses exactly as before — the base URL token only
  changes how the request URL is composed, not how the response is parsed.
- **Custom mutators receive the composed URL.** The `${baseUrl}` prefix is
  applied to the route before it's handed to `generateMutatorConfig`, so a
  configured `mutator` sees the same fully composed URL a plain `HttpClient`
  call would use.
- **MSW mocks stay relative.** Mock route matching is unaffected by
  `override.angular.baseUrl` — MSW handlers keep matching on the route path,
  not the composed base URL.
- **Mutually exclusive with `output.baseUrl`.** `output.baseUrl` bakes a
  static prefix into every generated route string for *all* clients;
  combining it with `override.angular.baseUrl` would double-prefix (or
  conflict with) every URL, so Orval throws a config-time error if both are
  set on the same output. Remove `output.baseUrl` and use
  `providePetstoreBaseUrlResolver` if you need the equivalent of a
  runtime-configurable prefix.
- **`apiId` is always explicit.** It's never derived from the specification's
  `info.title` or the target file name, so renaming your spec or output file
  doesn't silently rename the generated DI tokens.
- Only the specification's top-level `servers` field is embedded as the
  fallback URL. If your specification sets `servers` per path/operation,
  the token still falls back to the spec-level `servers` entry (selected via
  `index`/`variables`), not a per-path override.

## httpResource Output (Angular v19.2+)

Enable the `httpResource` retrieval mode with `override.angular.retrievalClient`.
This mode targets Angular's `httpResource` API, which is available in Angular
v19.2+:

```ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      mode: 'tags-split',
      target: 'src/api/http-resource/petstore.ts',
      schemas: 'src/api/model',
      client: 'angular',
      override: {
        angular: {
          retrievalClient: 'httpResource',
        },
      },
    },
    input: {
      target: './petstore.yaml',
    },
  },
});
```

Generated resource functions use signals and return `HttpResourceRef<T>`:

```ts
export function showPetByIdResource(
  petId: Signal<string>,
): HttpResourceRef<Pet | undefined> {
  return httpResource<Pet>(() => `/pets/${petId()}`);
}
```

Because Orval returns Angular's native `HttpResourceRef`, you automatically get
Angular's resource APIs such as `hasValue()`, `status()`, `error()`,
`reload()`, and the resource snapshot API for advanced composition.

### Which operations become `httpResource` functions?

Orval generates `httpResource` helpers for retrieval-style operations.

- `GET` operations are generated as resources
- retrieval-style `POST` operations, such as search/list/find/query endpoints,
  can also be generated as resources
- mutation-style operations remain `HttpClient` service methods

If you need to override the default classification for a specific operation, use
an operation-level Angular override:

```ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      client: 'angular',
      target: 'src/api/petstore.ts',
      override: {
        angular: {
          retrievalClient: 'httpResource',
        },
        operations: {
          searchPets: {
            angular: {
              retrievalClient: 'httpResource',
            },
          },
        },
      },
    },
    input: {
      target: './petstore.yaml',
    },
  },
});
```

This is especially useful when your API uses `POST` for retrieval-style
endpoints.

### Consume generated resources safely

Guard `value()` reads with `hasValue()`. Angular resources throw if you read
`value()` while the resource is in an error state, so `hasValue()` is the safe
and recommended gate.

```ts
import { computed, signal } from '@angular/core';

import { showPetByIdResource } from './api/http-resource/pets.service';

const petId = signal('1');
const petResource = showPetByIdResource(petId);

const petName = computed(() => {
  if (!petResource.hasValue()) {
    return undefined;
  }

  return petResource.value().name;
});
```

<Callout title="Tip" type="info">
Prefer returning a fallback from a `computed()` when `hasValue()` is false
instead of reading `value()` unconditionally. That keeps loading and error
states safe in templates and component logic.

### Multiple content types

When an operation can return multiple response content types, generated
resources may expose an `accept` parameter and return different result types for
different `Accept` values.

```ts
export function showPetByIdResource(
  petId: Signal<string>,
  accept: 'application/json',
): HttpResourceRef<Pet | undefined>;

export function showPetByIdResource(
  petId: Signal<string>,
  accept: 'text/plain',
): HttpResourceRef<string | undefined>;
```

This keeps the generated Angular API aligned with your OpenAPI response matrix,
including plain-text, blob, and array-buffer resource variants where needed.

Content-negotiated resources are just as reactive as single-content-type ones:
every signal input — path params, the `params` query object, `version`, and
request-body signals — is read inside the `httpResource` factory, so updating
any of them triggers a refetch. The `accept` argument itself is a plain value,
not a signal: it only selects which resource variant (`httpResource`,
`.text`, or `.arrayBuffer`) gets created, so to switch content types you
create a new resource rather than mutating a signal.

<Callout title="Tip" type="info">
Because `accept` is read once at creation time, don't wrap it in a signal —
call the resource function again with a different `accept` value if you need
to switch content types at runtime.

### `both` mode

Use `both` when you want signal-first retrievals and imperative `HttpClient`
methods in the same generated area:

```ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      mode: 'tags-split',
      target: 'src/api/petstore.ts',
      schemas: 'src/api/model',
      client: 'angular',
      override: {
        angular: {
          retrievalClient: 'both',
        },
      },
    },
    input: {
      target: './petstore.yaml',
    },
  },
});
```

In `both` mode, Orval keeps the service class in your main generated file and
emits the retrieval resources in a sibling `*.resource.ts` file.

```ts
import { PetstoreService } from './api/petstore';
import { listPetsResource } from './api/petstore.resource';
```

This separation works well when your app prefers signal-first reads but still
needs service methods for writes or imperative request flows.

### httpResource options

You can customize generated `httpResource` calls through
`override.angular.httpResource`:

```ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      mode: 'tags-split',
      target: 'src/api/http-resource/petstore.ts',
      schemas: {
        type: 'zod',
        path: 'src/api/model-zod',
      },
      client: 'angular',
      override: {
        angular: {
          retrievalClient: 'httpResource',
          httpResource: {
            defaultValue: { id: 'fallback' },
            debugName: 'getPetByIdResource',
          },
        },
      },
    },
    input: {
      target: './petstore.yaml',
    },
  },
});
```

Common options:

- `defaultValue` — initial value exposed while the resource is idle/loading
- `debugName` — name shown in Angular DevTools

Advanced options:

- `injector` — raw expression passed to `HttpResourceOptions.injector`
- `equal` — raw expression passed to `HttpResourceOptions.equal`

For more advanced cases, you can also pass raw expressions for Angular's
resource options:

```ts
override: {
  angular: {
    retrievalClient: 'httpResource',
    httpResource: {
      injector: 'inject(Injector)',
      equal: '(a, b) => a?.id === b?.id',
    },
  },
}
```

Operation-level `httpResource` options override global ones:

```ts
override: {
  angular: {
    retrievalClient: 'httpResource',
    httpResource: {
      debugName: 'globalPetResource',
    },
  },
  operations: {
    showPetById: {
      angular: {
        retrievalClient: 'httpResource',
        httpResource: {
          debugName: 'showPetByIdResource',
        },
      },
    },
  },
}
```

Orval also emits a shared helper type for these options. The exact default
generic for `TOmitParse` depends on the generated file, but the emitted shape
is:

```ts
export type OrvalHttpResourceOptions<
  TValue,
  TRaw = unknown,
  TOmitParse extends boolean = false,
> = (TOmitParse extends true
  ? Omit<HttpResourceOptions<TValue, TRaw>, 'parse'>
  : HttpResourceOptions<TValue, TRaw>) &
  OrvalHttpResourceRequestExtension;
```

When `defaultValue` is configured, generated resource overloads return
`HttpResourceRef<T>` instead of `HttpResourceRef<T | undefined>`.

### Request headers and HttpContext

Generated `httpResource` helpers can expose the same request-descriptor
surface as the generated `HttpClient` service methods: OpenAPI `in: header`
parameters, plus a call-site escape hatch for extra headers and Angular's
`HttpContext` (for example to flag a request for a global HTTP interceptor).

**OpenAPI `in: header` parameters.** When the top-level `output.headers` option
is enabled, header parameters declared in your spec are generated as a
`headers: Signal<XHeaders>` argument and forwarded on every request:

```ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      target: 'src/api/http-resource/petstore.ts',
      schemas: 'src/api/model',
      client: 'angular',
      headers: true, // generate `in: header` parameters
      override: {
        angular: {
          retrievalClient: 'httpResource',
        },
      },
    },
    input: {
      target: './petstore.yaml',
    },
  },
});
```

```ts
const correlationId = signal('abc-123');
const headers = computed(() => ({ 'X-Correlation-Id': correlationId() }));

listPetsResource(params, headers);
```

**Extra headers and `HttpContext` at the call site.** Every generated
`httpResource` helper's trailing options bag also accepts `headers`,
`context`, and `request` — independent of whether the operation declares any
`in: header` parameters. This mirrors the `HttpClientOptions` already
available on generated `HttpClient` service methods, so you no longer have to
drop down to `HttpClient` just to attach interceptor policy or a
per-request header.

The extension fields live on the **trailing options parameter** — the same
bag that carries `defaultValue`/`debugName`. Pass every other generated
argument first: path/query signals, an `in: header` signal when
`output.headers` is enabled, and the `accept` argument on multi-content
helpers all come before the options bag.

```ts
import { HttpContext } from '@angular/common/http';
import { BYPASS_GLOBAL_ERROR_HANDLER } from './http-context-tokens';

const requestId = signal('req-42');

// operation with a single path param and one content type:
// the options bag is the second argument
const itemEvents = getItemEventsResource(itemId, {
  headers: () => ({ 'x-request-id': requestId() }),
  context: new HttpContext().set(BYPASS_GLOBAL_ERROR_HANDLER, true),
});

// multi-content operation from the petstore sample
// (signature: petId, accept, version?, options?) — fill every generated
// parameter before the options bag, using `undefined` for optional ones
// you want to skip
const pet = showPetByIdResource(petId, 'application/json', undefined, {
  context: new HttpContext().set(BYPASS_GLOBAL_ERROR_HANDLER, true),
});
```

- `headers` — extra headers merged over any generated headers. Accepts either
  an `HttpHeaders` instance / plain record, or a function returning one.
- `context` — an Angular `HttpContext` forwarded to the underlying request.
  Accepts either an `HttpContext` instance or a function returning one.
- `request` — a last-resort escape hatch that receives the fully-built
  `HttpResourceRequest` and returns the request Orval actually sends.

> **Note:**
> Pass the function form (`headers: () => ...`, `context: () => ...`, or
> `request: (req) => ...`) whenever the value reads a signal. Orval invokes
> these functions **inside** the `httpResource` reactive factory, so the
> resource automatically reloads when the signals they read change. A plain
> object or `HttpContext` instance is read once per request computation and
> does not itself trigger a reload.

**Precedence.** Generated OpenAPI headers are applied first, extra `headers`
from the options bag are merged on top (overriding any header with the same
name), and `request` runs last. For operations with multiple success content
types, the generated `Accept` header is always re-applied after your
extension so response-branch dispatch (the `accept` argument) stays correct —
your `headers`/`context`/`request` can add or override any other header, but
never `Accept`.

Orval emits the extension as a small, reusable surface alongside
`OrvalHttpResourceOptions`:

```ts
export interface OrvalHttpResourceRequestExtension {
  headers?: HttpResourceRequest['headers'] | (() => HttpResourceRequest['headers']);
  context?: HttpContext | (() => HttpContext);
  request?: (request: HttpResourceRequest) => HttpResourceRequest;
}
```

`applyOrvalRequestExtension` is the emitted helper that applies these fields;
you won't normally call it directly, but you may see it referenced from
`httpResource` factories in the generated output.

Testing works the same way as any other generated `httpResource` call — with
`HttpTestingController`, assert on the intercepted request's `headers` and
`context`:

```ts
const req = httpMock.expectOne('/items/42/events');
expect(req.request.headers.get('x-request-id')).toBe('req-42');
expect(req.request.context.get(BYPASS_GLOBAL_ERROR_HANDLER)).toBe(true);
```

### Mutator compatibility

`httpResource` output supports request-compatible mutators, but it cannot use
`HttpClient`-style mutators that require the generated `HttpClient` instance as
an extra argument. If your mutator depends on that injected `HttpClient`
parameter, prefer `httpClient` or `both` mode for that operation.

## httpResource + Zod schema types

When `schemas.type` is set to `zod`, Orval can generate Angular `httpResource`
functions alongside the Zod-backed model files. The generated resources still
return Angular's native `HttpResourceRef`, so the main best-practice remains the
same: guard `value()` reads with `hasValue()` and test loading/error states the
same way you would for any other `httpResource`.

With `override.angular.runtimeValidation: true`, generated JSON resources also
emit `parse: Schema.parse` and expose Zod output types such as `PetOutput` and
`PetsOutput`.

> **Note:**
> Zod is a runtime dependency when you enable `schemas.type: 'zod'`.
> Make sure your app installs `zod` (for example, `zod` in `dependencies`).

```ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      mode: 'tags-split',
      target: 'src/api/http-resource-zod/petstore.ts',
      schemas: {
        type: 'zod',
        path: 'src/api/model-zod',
      },
      client: 'angular',
      override: {
        angular: {
          retrievalClient: 'httpResource',
          runtimeValidation: true, // opt-in
        },
      },
    },
    input: {
      target: './petstore.yaml',
    },
  },
});
```

## Testing generated `httpResource` functions

Generated resources use Angular's standard `HttpClient` stack, so test them the
same way you test any other `HttpClient`-based code: configure
`provideHttpClient()` before `provideHttpClientTesting()`, flush requests with
`HttpTestingController`, then wait for Angular to propagate the new signal
values.

```ts
import { provideHttpClient } from '@angular/common/http';
import {
  HttpTestingController,
  provideHttpClientTesting,
} from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';

import { PetsPage } from './pets.page';

describe('PetsPage', () => {
  let httpMock: HttpTestingController;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [PetsPage],
      providers: [provideHttpClient(), provideHttpClientTesting()],
    }).compileComponents();

    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('renders fetched pets', async () => {
    const fixture = TestBed.createComponent(PetsPage);
    fixture.detectChanges();

    const req = httpMock.expectOne('/v1/pets');
    expect(req.request.method).toBe('GET');
    req.flush([{ id: 1, name: 'Rex', requiredNullableString: null }]);

    await fixture.whenStable();
    fixture.detectChanges();

    expect(fixture.nativeElement.textContent).toContain('Rex');
  });
});
```

This is also the right place to verify loading and backend-error behavior for
`schemas.type: 'zod'` outputs, especially when your component uses guarded
`hasValue()` checks to avoid unsafe `value()` reads. When
`override.angular.runtimeValidation` is enabled, add at least one test that
flushes an invalid JSON payload and asserts that the resource exposes a
`ZodError`.

## Zod Runtime Validation

Angular output supports runtime validation for Zod-backed responses when
`schemas.type` is set to `zod` and `override.angular.runtimeValidation` is
enabled. For JSON model responses, the default `throw` strategy runs
`Schema.parse()` on eligible responses. The `both` strategy uses
`safeParse()`, logs the raw `ZodError` with `console.error`, then re-throws.

This applies to generated Angular `HttpClient` services too, not just
`httpResource`. If you keep the default Angular retrieval mode
(`retrievalClient: 'httpClient'`), eligible generated `GET` methods already
pipe JSON model responses through Zod.

### Setup

1. Set `schemas.type` to `zod` to generate Zod schemas
2. Set `override.angular.runtimeValidation` to either:
   - `true` (shorthand for `{ strategy: 'throw' }`)
   - `{ strategy: 'both' }` for log-then-throw behavior

```ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      mode: 'tags-split',
      target: 'src/api/petstore.ts',
      schemas: {
        type: 'zod',
        path: 'src/api/schemas',
      },
      client: 'angular',
      override: {
        angular: {
          runtimeValidation: { strategy: 'both' },
        },
      },
    },
    input: {
      target: './petstore.yaml',
    },
  },
});
```

### How It Works

For generated `HttpClient` services, JSON responses are validated in the RxJS
pipeline. Generated `httpResource` JSON retrievals use Angular's
`HttpResourceOptions.parse` hook with the same strategy semantics.

```ts
// Generated code (simplified)
getPet(options?: HttpClientBodyOptions): Observable<PetOutput> {
  return this.http
    .get<PetOutput>(`/pet`, { observe: 'body' })
    .pipe(map((data) => Pet.parse(data)));
}

showPetByIdResource(
  petId: Signal<string>,
): HttpResourceRef<PetOutput | undefined> {
  return httpResource<PetOutput>(() => `/pet/${petId()}`, {
    parse: Pet.parse,
  });
}
```

**Validation applies to:**
- JSON model responses in generated `HttpClient` services
- JSON model responses returned by generated `GET` methods
- JSON model responses returned by generated mutation methods such as `POST`, `PUT`, `PATCH`, and `DELETE`
- JSON `httpResource` retrievals with model response types

**Validation is skipped for:**
- Primitive types (`string`, `number`, `boolean`, `void`, `unknown`)
- Custom mutator paths
- Non-JSON content types (text, blob, arrayBuffer)

For generated `HttpClient` services, runtime validation also applies when the
JSON response body is surfaced through `response` and response-event observe
flows.

### Request bodies are typed, but not auto-parsed

When you generate Zod schemas, Orval also emits request-body schemas such as
`CreatePetsBody`, plus matching `z.input<>` / `z.output<>` types. Generated
Angular `HttpClient` methods use those types, but they do **not** automatically
call `Schema.parse(body)` before sending a request.

That means the current Angular + Zod split is:

- request bodies get generated TypeScript types and reusable Zod schemas
- response bodies get automatic runtime parsing when
  `override.angular.runtimeValidation` is enabled

If you want pre-send validation, validate explicitly before calling the
generated client:

```ts
const payload = CreatePetsBody.parse(formValue);
return this.petsService.createPets(payload);
```

Use `.parse()` when you want invalid input to throw immediately, or
`.safeParse()` when you want to surface validation feedback without throwing.

## Request-body readonly guidance

Angular output also supports `override.preserveReadonlyRequestBodies` to
control how OpenAPI `readOnly` fields are handled in generated request body
types:

- `'strip'` (default) — recommended for most OpenAPI specs
- `'preserve'` — use when your request DTOs are intentionally immutable

```ts
export default defineConfig({
  petstore: {
    output: {
      override: {
        preserveReadonlyRequestBodies: 'strip',
      },
    },
  },
});
```

`'strip'` is the best default because OpenAPI `readOnly` properties are usually
response-oriented. The same guidance applies to both Angular `HttpClient` and
`httpResource` output: `httpResource` can still issue request payloads, so it
should not preserve readonly request fields by default.

### Runtime validation support matrix

| Client | Config key | Runtime validation |
|--------|------------|--------------------|
| `angular` | `override.angular.runtimeValidation` | ✅ Supported |
| `angular-query` | `override.query.runtimeValidation` | ✅ Supported |
| `fetch` | `override.fetch.runtimeValidation` | ✅ Supported |
| custom mutator paths | varies | ⚠️ See [#2858](https://github.com/orval-labs/orval/issues/2858) |

### Error Handling

When validation fails, Zod throws a `ZodError`.

For `HttpClient` services, handle it in your subscription, effect, or error
interceptor:

```ts
this.petService.getPet().subscribe({
  next: (pet) => console.log('Validated pet:', pet),
  error: (err) => {
    if (err.name === 'ZodError') {
      console.error('Response validation failed:', err.issues);
    }
  },
});
```

For generated `httpResource` helpers, read the error through the resource's
`error()` signal and keep `value()` reads guarded with `hasValue()`.

## Query parameter filtering

Angular's `HttpParams` only accepts primitives (`string`, `number`, `boolean`,
and arrays of those). Anything else — objects, arrays of objects, `null`,
`undefined` — is rejected or silently stringified to `"[object Object]"`. To
make `HttpParams` safe, the generated Angular client passes every query
parameter through an internal `filterParams` helper before handing it to
`HttpClient`.

### Default behaviour (schema-aware passthrough)

1. **Primitive params** keep the existing nullish-stripping, with one
   exception: a param the spec declares both `required` and `nullable`.
   Silently dropping a required param would send a request that violates the
   OpenAPI contract, so it is never simply removed. Without a
   `paramsSerializer`, a `null` value for such a param is sent as an empty
   string (`?key=`) so the key still reaches the wire. With a
   `paramsSerializer` configured, the literal `null` is passed through for
   the serializer to encode instead. Optional nullable params are still
   dropped when `null` — only required-nullable params get this treatment.
   See [issue #3712](https://github.com/orval-labs/orval/issues/3712).
2. **Schema-declared object params** are serialized according to the OpenAPI
   parameter's `style`/`explode` (see
   [Object query parameters](#object-query-parameters-style--explode) below)
   **when no `paramsSerializer` is configured**. When a `paramsSerializer`
   *is* configured, the object is instead passed through untouched — that
   serializer becomes the consumer responsible for turning the raw object
   into something `HttpParams` can send.
3. **Arrays of objects** are passed through untouched **only when a
   `paramsSerializer` is configured**; without one (or a `paramsFilter`, see
   below) there is no consumer that can handle a raw object, so the param is
   dropped. OpenAPI defines no style/explode encoding for arrays of objects,
   so — unlike plain object params — this case is unaffected by the
   object-serialization behavior above.

```yaml
parameters:
  - name: filters
    in: query
    schema:
      type: object
      additionalProperties:
        type: string
```

With the spec above and **no** `paramsSerializer` configured, the generated
client now serializes `filters` per the OpenAPI default (`style: form`,
`explode: true`) — spreading its properties as top-level query params. With a
configured `paramsSerializer` (e.g. `qs.stringify`), the generated client
instead preserves `filters` as a raw object and forwards it to the serializer.
If you need the raw object even without a serializer — for example a
`mutator` that flattens it into bracketed keys — use `override.paramsFilter`
(see below). See
[issue #3326](https://github.com/orval-labs/orval/issues/3326) and
[issue #3705](https://github.com/orval-labs/orval/issues/3705) for the
motivating bugs.

### Object query parameters (style & explode)

> Added in the fix for
> [issue #3705](https://github.com/orval-labs/orval/issues/3705).

OpenAPI's default for an `in: query` parameter is `style: form` +
`explode: true`. For a `type: object` schema that means the object's
properties are spread as separate top-level query params — not nested under
the parameter's own name. Orval's generated Angular client now honors this
(and the `form` + `explode: false` and `deepObject` variants) at request time,
as long as no `paramsSerializer`/`paramsFilter` is configured for the
operation.

```yaml
paths:
  /catalog-items:
    get:
      operationId: searchCatalog
      parameters:
        - name: arg0
          in: query
          schema:
            type: object
            properties:
              itemReferences:
                type: array
                items:
                  type: string
              pageNumber:
                type: integer
```

```ts
export type SearchCatalogParams = {
  arg0?: {
    itemReferences?: string[];
    pageNumber?: number;
  };
};
```

```ts
searchCatalog({ arg0: { itemReferences: ['a', 'b'], pageNumber: 1 } });
// -> GET /catalog-items?itemReferences=a&itemReferences=b&pageNumber=1
```

The generated parameter **type** intentionally keeps the shape your spec
declares — `arg0` stays nested, mirroring the parameter name reported by your
tooling. Orval does not invent a flatter name for it, since that name is
usually an artifact of the source toolchain rather than something meaningful
in your API. If `arg0` (or similar) is showing up because your Spring/
springdoc-based backend isn't compiled with parameter names preserved, fix it
at the source instead: enable the `-parameters` javac flag (or annotate the
controller method parameter with `@ParameterObject` from
`springdoc-openapi`) so the generated spec reports the real parameter name.

| `style` | `explode` | Wire format | Strategy |
| --- | --- | --- | --- |
| `form` (default) | `true` (default) | `itemReferences=a&itemReferences=b&pageNumber=1` | `flatten` |
| `form` | `false` | `arg0=itemReferences,a,b,pageNumber,1` | `comma` |
| `deepObject` | (any) | `arg0[itemReferences]=a&arg0[itemReferences]=b&arg0[pageNumber]=1` | `deepObject` |

Nested non-primitive values inside an exploded/deepObject-serialized object
(an object-in-object, or an array of objects) are dropped using the same
primitive-only rules the top-level filter already applies — OpenAPI does not
define a style/explode encoding for them either, so they never reach the
wire as `"[object Object]"`.

To restore the pre-#3705 behavior (object-typed query params silently
dropped), set:

```ts
export default defineConfig({
  petstore: {
    output: {
      client: 'angular',
      target: './src/api/petstore.ts',
      override: {
        angular: {
          queryObjectSerialization: 'legacy',
        },
      },
    },
  },
});
```

`queryObjectSerialization` can be set globally, per-tag, or per-operation,
just like `retrievalClient`/`runtimeValidation`. See the
[configuration reference](/docs/reference/configuration/output#queryobjectserialization).

### Custom filter with `override.paramsFilter`

When the schema cannot describe what you need — for example you want to
preserve empty strings, encode arrays differently, or rename keys before
they hit the wire — supply a `paramsFilter` mutator. It **completely
replaces** the built-in `filterParams` for every operation that resolves
to it; you own nullish-stripping and any non-primitive handling.

Even though `paramsFilter` is Angular-only, it lives at `override.paramsFilter`
instead of `override.angular.paramsFilter` so you can keep using the standard
global / per-operation / per-tag override pattern that Orval already uses for
request shaping.

```ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      client: 'angular',
      target: './src/api/petstore.ts',
      override: {
        paramsFilter: {
          path: './src/api/params-filter.ts',
          name: 'filterPetstoreParams',
        },
      },
    },
  },
});
```

```ts
// You are now the filter. orval will not strip null, undefined, or object
// values for you — return exactly the shape Angular's HttpParams should see.
export const filterPetstoreParams = (
  params: Record<string, unknown>,
): Record<string, unknown> => {
  const result: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(params)) {
    if (value === undefined) continue;
    result[key] = value;
  }
  return result;
};
```

#### What you take over when you opt in

The built-in helper is **not** called alongside your filter. In particular,
you are responsible for:

- stripping `undefined` (Angular's `HttpParams` does not safely represent it for query encoding; handle it explicitly)
- handling `null` (Angular treats it as the literal string `"null"`)
- filtering arrays (`new HttpParams({ fromObject: { tags: [{}] } })` is
  invalid)
- serializing object-valued params per their `style`/`explode` (or coercing/
  flattening them yourself) — or relying on a downstream `paramsSerializer`
  to do it
- the required-nullable empty-string fallback described above — opting into
  `paramsFilter` also replaces that behavior, so if you prefer the old
  drop-silently semantics for required-nullable params, `paramsFilter` is
  the escape hatch to restore it

The exact rules the default helper applies — useful as a starting point you
can adapt:

```ts
// This is the logic orval emits when no `paramsFilter` is configured.
// `passthroughKeys` is only non-empty when a `paramsSerializer` is also
// configured (it carries the schema-declared object/array-of-object keys).
// `objectParamStrategies` carries the per-key style/explode strategy
// (issue #3705) and is empty when a `paramsSerializer` is configured or
// `queryObjectSerialization: 'legacy'` is set.
// `preserveRequiredNullables` is true only when a `paramsSerializer` is
// configured — that's what lets the literal `null` survive for the
// serializer to encode; without one, a required-nullable `null` becomes an
// empty string so the key still reaches the wire (see #3712).
// Copy/adapt the parts you want; drop the rest.
type Primitive = string | number | boolean;
type Value = Primitive | Primitive[] | null;
type ObjectParamStrategy = 'flatten' | 'comma' | 'deepObject';

export const defaultFilter = (
  params: Record<string, unknown>,
  requiredNullableKeys = new Set<string>(),
  preserveRequiredNullables = false,
  passthroughKeys = new Set<string>(),
  objectParamStrategies: Readonly<Record<string, ObjectParamStrategy>> = {},
): Record<string, Value | unknown> => {
  const out: Record<string, Value | unknown> = {};
  const filterPrimitives = (value: unknown[]) =>
    value.filter(
      (item): item is Primitive =>
        item != null &&
        (typeof item === 'string' ||
          typeof item === 'number' ||
          typeof item === 'boolean'),
    );
  for (const [key, value] of Object.entries(params)) {
    if (passthroughKeys.has(key)) {
      if (value !== undefined) out[key] = value;
      continue;
    }
    const strategy = objectParamStrategies[key];
    if (strategy && value != null && typeof value === 'object' && !Array.isArray(value)) {
      const entries = Object.entries(value as Record<string, unknown>);
      if (strategy === 'comma') {
        const parts = entries
          .filter(([, v]) => v != null && typeof v !== 'object')
          .flatMap(([k, v]) => [k, String(v)]);
        if (parts.length) out[key] = parts.join(',');
      } else {
        for (const [prop, propValue] of entries) {
          const targetKey = strategy === 'deepObject' ? `${key}[${prop}]` : prop;
          if (Array.isArray(propValue)) {
            const filtered = filterPrimitives(propValue);
            if (filtered.length) out[targetKey] = filtered;
          } else if (propValue != null && typeof propValue !== 'object') {
            out[targetKey] = propValue as Value;
          }
        }
      }
      continue;
    }
    if (Array.isArray(value)) {
      const filtered = filterPrimitives(value);
      if (filtered.length) out[key] = filtered;
    } else if (value === null && requiredNullableKeys.has(key)) {
      out[key] = preserveRequiredNullables ? null : '';
    } else if (
      value != null &&
      (typeof value === 'string' ||
        typeof value === 'number' ||
        typeof value === 'boolean')
    ) {
      out[key] = value;
    }
  }
  return out;
};
```

#### Composing with `paramsSerializer`

If you configure both, orval calls them in this order so the serializer
operates on the filtered result:

```
raw params → paramsFilter → paramsSerializer → HttpParams
```

`paramsFilter` controls *which keys/values reach the request*;
`paramsSerializer` controls *how the surviving values are encoded into the
URL*. Both are optional and independent. Configuring a `paramsSerializer`
also disables the built-in object-serialization strategies described in
[Object query parameters](#object-query-parameters-style--explode) above —
the raw object is passed through to your serializer instead.

## Advanced helpers

Generated `httpResource` files also include small convenience helpers such as
`ResourceState<T>` and `toResourceState()` for integrating Angular resources
into your own state abstractions.

## Full Example

See the [complete Angular example](https://github.com/orval-labs/orval/tree/master/samples/angular-app) on GitHub.
