OdataDuty::OAS2.build_json(schema) renders your schema as an OpenAPI 2.0 (Swagger) document. The Rails integration serves it at /$oas2, so tools that consume Swagger — API explorers, code generators, and low-code platforms like Microsoft Power Automate — can build a client from your service.
OAS2.build_json requires a builder-DSL schema (SchemaBuilder.build), not a class-based Schema.
A description: declared anywhere in the schema (see
doc/using_descriptions.md) flows into this document: a schema
description becomes info.description; entity/complex/enum type descriptions become
definitions.<Type>.description; property descriptions become
definitions.<Type>.properties.<name>.description; and an entity-set description adds summary
(the same verb text as the matching MCP tool) plus description to every operation for that set
(GET collection and individual, POST, PATCH, DELETE). A set with no description keeps
today's output exactly — no summary or description key on its operations.
The GET operation on a collection path advertises a query-option parameter only when the set's
resolver can actually serve it, so the document never offers an option the service would reject
with NoImplementationError:
| Parameter | Emitted when the resolver defines |
|---|---|
$filter |
any public od_filter_* hook — see doc/using_filter.md |
$search |
od_search |
$select |
always — the projection happens regardless, so it needs no hook |
$top |
od_top |
$skip |
od_skip |
$count |
count |
$skiptoken |
od_skiptoken |
$filter is gated on any od_filter_* hook, including od_filter_or on its own: its presence
means the set filters, not that every property and operator combination is implemented. The value
stays a freeform string.
Some parameters carry constraints beyond their type, so a generated client can reject bad input
before it reaches your service: $select is a csv array whose enum names the entity type's
properties, and $top/$skip are bounded at zero.
A set implementing only collection therefore renders a single parameter — $select — on its
collection GET.
None of this changes request handling: omitting a parameter from $oas2 does not stop the service
from honoring, or rejecting, that query option when a client sends it anyway. $metadata is
untouched by the gating too — the Capabilities.FilterRestrictions annotation still keys off
od_filter_or alone, and a set with no filter hooks emits no Filterable: false. The MCP tool
schemas apply the same gating to the six options that have an odata_* spelling — all but
$count, which surfaces as a separate count_<Set> tool — described in
doc/using_mcp.md; unlike $oas2, those cover both DSLs.
Note that working out these gates constructs every collection resolver, running its
od_after_init with the set's init_args, purely to see which hooks it answers to. Both the
generated controller and the examples here call build_json(schema) with no context:, so a
resolver whose od_after_init reads the request context will fail when /$oas2 is rendered —
pass context: as the data endpoints do if yours needs it.
Power Automate (and Power Apps) can turn the document into a custom connector. It accepts OpenAPI 2.0 only (not 3.0), which is exactly what OdataDuty emits, and the document must be under 1 MB. The rendered size grows with your schema (entity types, properties, enums), so a very large schema could approach that limit — check the byte size if you have hundreds of entities.
There are two ways to import, and they behave differently:
- Import from OpenAPI file — you download the
$oas2JSON and upload it. This always works. - Import from URL — you paste the
$oas2URL. This is a browser-side, cross-origin fetch made frommake.powerautomate.com/make.powerapps.com, so it is subject to CORS.
Because "Import from URL" fetches from the browser, your server must return an Access-Control-Allow-Origin header on the $oas2 response. Without it, the browser refuses to hand the response body to Power Automate and you get a misleading error:
We weren't able to download the OpenAPI file from the provided URL. Please upload the file manually using the "Import from OpenAPI file" option.
This error is easy to misdiagnose: the request reaches your server and returns 200 (you will see it in your logs), so it looks like a download or content problem when it is really a missing CORS header. The generated Rails controller sets it for you:
def oas2
# Power Automate's "Import from URL" is a browser-side cross-origin fetch; without this CORS
# header it fails with a misleading "couldn't download" error even though the server 200s.
response.set_header('Access-Control-Allow-Origin', '*')
render json: OdataDuty::OAS2.build_json(schema)
end* is safe here: $oas2 is public metadata fetched without credentials. If you prefer to scope it, allow the Power Platform origins instead:
POWER_PLATFORM_ORIGINS = %w[
https://make.powerapps.com
https://make.powerautomate.com
https://make.preview.powerapps.com
].freeze
def oas2
origin = request.headers['Origin']
if POWER_PLATFORM_ORIGINS.include?(origin)
response.set_header('Access-Control-Allow-Origin', origin)
end
render json: OdataDuty::OAS2.build_json(schema)
endA plain GET of $oas2 is a CORS "simple request", so no OPTIONS preflight is sent — the response header above is all that is required. Add Access-Control-Allow-Methods / Access-Control-Allow-Headers only if you later expose the data endpoints to browser clients that trigger a preflight.
If Power Automate's "Import from URL" fails, work through it in this order — the failure is almost always transport, not the document:
- Does file import work? Download the JSON and use "Import from OpenAPI file". If that succeeds, the document is valid and the problem is the fetch (keep going).
- Is the endpoint reachable anonymously?
curlit with no auth; you want200 application/json. - Does the request reach your server? Check your logs. A logged
200with the import still failing points squarely at CORS. - Is the CORS header present?
curl -I -H 'Origin: https://make.powerapps.com' <url> | grep -i access-controlshould echo anAccess-Control-Allow-Origin.