Rules and extensions¶
okapipy gets a long way by reading the spec — plurals are collections,
verbs are actions, {id} segments are resources. When that's not enough
(or when you don't own the spec), there are two override layers:
x-okapipy-*extensions baked into the spec itself, and- A project-local rules file that mirrors the same shape and wins on conflict.
The rules file is the right tool when you can't (or shouldn't) edit the upstream OpenAPI document. The extensions are the right tool when you do own the spec and want the hint to live next to the path it classifies.
Classification, in order¶
For each path segment, okapipy checks:
- Is it
{something}? → resource identifier. - Is there an explicit
x-okapipy-kind(rules file first, then spec)? → use it. - Is the segment listed in the namespace registry? → namespace.
- What does spaCy say? Plural noun → collection; verb (or verb-phrase) → action.
- Fallback → collection, with a warning.
Allowed x-okapipy-kind values: namespace, collection, action,
singleton. Unknown values are rejected at load time, with the path that
contains them.
x-okapipy-ns — declare your folders¶
x-okapipy-ns is a root-level list of path prefixes that should be
treated as namespaces. Without it, a singular noun like commerce or
auth only becomes a namespace when the heuristic guesses correctly —
the registry makes it deterministic.
openapi: 3.0.0
info:
title: Commerce API
version: 1.0.0
x-okapipy-ns:
- commerce
- commerce/internal # nested namespaces are fine
- settings
paths:
/commerce/orders: { ... } # → Namespace(commerce) → Collection(Orders)
/commerce/internal/audit: { ... } # → ns(commerce) → ns(internal) → Collection(Audit)
Leading slashes are tolerated — /commerce and commerce are the same
thing.
x-okapipy-kind — override a single segment¶
Two scopes:
- Path-item level — classifies the segment itself, and propagates to any other path that walks through the same prefix.
- Operation level — narrower; routes a single method to a synthetic Action without changing the segment's classification.
paths:
/me:
x-okapipy-kind: singleton # /me is a Singleton, not a Namespace
get: { summary: Return the current user, responses: { '200': ... } }
patch: { summary: Update the current user, responses: { '200': ... } }
/orders/{id}/submit:
post:
x-okapipy-kind: action # operation-level: route this POST to an Action
/staff: # spaCy treats "staff" as singular → Namespace
x-okapipy-kind: collection
get: { ... }
Declaring /me as a singleton once is enough — /me/notifications,
/me/refresh, etc. will all see it.
x-okapipy-exclude — skip operations¶
Path-item level. Drops operations during parsing, useful for endpoints that shouldn't appear in the generated client:
paths:
/internal/debug:
x-okapipy-exclude: "*" # drop every method on this path
/orders/{id}:
x-okapipy-exclude: [DELETE] # drop just DELETE; keep GET / PUT / PATCH
get: { ... }
delete: { ... }
Method names are case-insensitive.
x-okapipy-paginated — opt out of pagination¶
Path-item or operation level. Defaults to true; set it to false for
list-shaped endpoints that return a bounded list (enum-like reference
data, configuration, etc.).
paths:
/currencies:
x-okapipy-paginated: false # the full list is short and fixed
get:
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Currency'
The rules file¶
When you can't edit the upstream spec, write the same hints in a JSON or
YAML file and point --rules at it. Rules-file values win on every
conflict with the spec.
# okapipy.rules.yaml — same shape as the extensions, lifted to the root.
x-okapipy-ns:
- commerce
- commerce/internal
- settings
paths:
/staff:
x-okapipy-kind: collection # path-item-level kind override
/me:
x-okapipy-kind: singleton # force /me to be a singleton
/orders/{id}/submit:
post:
x-okapipy-kind: action # per-method kind override
/currencies:
x-okapipy-paginated: false # per-path pagination override
/internal/debug:
x-okapipy-exclude: "*" # drop every method
/orders/{id}:
x-okapipy-exclude: [DELETE] # drop just selected methods
Then:
okapipy spec parse openapi.yaml --rules okapipy.rules.yaml
okapipy spec generate openapi.yaml --rules okapipy.rules.yaml \
--output ./my-client --package acme.commerce --client-class CommerceClient
The rules file is local-only — no http(s) URLs. That's a deliberate constraint; project-local overrides should live in your repo, not on a remote server you don't control.
Worked examples¶
"spaCy keeps tagging my collection as a namespace"¶
The token looks singular to spaCy — typically because the lemma matches a
common singular noun in English (staff, news, series, species).
Force it:
"I want a custom verb to be an Action"¶
If the verb isn't in the small built-in registry (login, logout,
refresh, revoke, verify, …), spaCy will probably tag it as a noun.
Pick the smallest possible scope:
If every method on the path is the action (rare), use path-item level:
paths:
/orders/{id}/escalate:
x-okapipy-kind: action
post: { ... }
get: { ... } # also routed to Action.operations
"I have many non-conforming endpoints and I don't want to annotate each one"¶
Some specs are full of operations that don't fit the
namespace/collection/resource/singleton/action hierarchy — PUT /users
for bulk updates, GET /admin/health, one-off RPCs that share a
collection path with regular CRUD. By default each one is dropped with
a warning, and the per-op fix is x-okapipy-kind: action on every one
of them.
When that's impractical — usually because you don't own the spec, or
there are too many — pass --unmatched <name> to keep them as flat
actions under a synthetic top-level namespace:
okapipy spec generate openapi.yaml \
--output ./my-client --package acme.commerce --client-class CommerceClient \
--unmatched ops
Now every otherwise-dropped operation lands under client.ops as its
own action, named after its operationId:
client.ops.bulk_update_users.run(body={...}) # PUT /users
client.ops.admin_health.run() # GET /admin/health
Operations without an operationId fall back to
<method>_<sanitized_path> — e.g. a bare PUT /users becomes
put_users. Two unmatched ops sharing an operationId produce
bulk_update, bulk_update_2, … and a parser warning so you can clean
up the spec.
The namespace name is CLI-only — there's no rules-file key for it,
because the choice is per-invocation, not a shared project setting.
<name> must not collide with the snake_case identifier of any
existing top-level node; on collision the generator exits non-zero with
UnmatchedNamespaceCollisionError and writes nothing.
Per-op x-okapipy-kind: action is still preferred when you own the spec
--unmatched is the bulk fallback — it puts everything under one
flat container. When you control the spec and there's a natural
home for the endpoint (under a collection, a resource, a
singleton), x-okapipy-kind: action keeps the operation in the
right place in the hierarchy.
"An endpoint shouldn't be in the client at all"¶
* is also accepted as the literal string "*" to drop every method;
otherwise pass an explicit list.
"This list is bounded and shouldn't paginate"¶
The generated fetch() returns the full list in a single request.
"A */current pseudo-resource has its own members and settings"¶
REST APIs commonly model "the current org" or "the current workspace" as
a singleton with its own sub-collections (/orgs/current/members,
/orgs/current/roles, /workspaces/current/tag-keys). Mark the
pseudo-resource as a singleton — okapipy hangs the sub-collections off
it directly.
paths:
/orgs/current:
x-okapipy-kind: singleton
# /orgs/current/members → MembersCollection on the OrgsCurrentSingleton
# /orgs/current/roles → RolesCollection on the OrgsCurrentSingleton
In the generated client, client.orgs.current.members is the collection
and client.orgs.current.members["usr_1"] is the per-member resource.
"An aggregate view on a collection (/orders/stats)"¶
Endpoints like /orders/stats, /datasets/summary, or
/secrets/encrypted are derived views of the parent collection — not
one of its items. Mark them as singletons; okapipy attaches them as
sub-singletons of the parent collection.
The generated client exposes client.orders.stats.retrieve() alongside
iteration over the collection itself.
"An /.well-known/... path crashes with an invalid Python identifier"¶
Path segments beginning with . (.well-known, .config) aren't valid
Python identifiers. okapipy expands a literal . to the word Dot so
the generated symbol stays valid: /.well-known/openid-configuration
becomes DotWellKnown (class) and dot_well_known (module/attribute).
No rule is needed; this is built in.
The raw segment is preserved on the parsed model and in the runtime URL
template, so HTTP routing keeps hitting /.well-known/... correctly.