Summary
When converting a CSDL action with non-nullable parameters (Nullable="false") to OpenAPI 3.0 via csdl2openapi, the generated requestBody schema lists every parameter under properties but does not emit a required array. As a result, downstream OpenAPI consumers (validators, code generators, LLM-based agents, doc UIs) treat all action parameters as optional — even though the OData service will reject the request with 400 Bad Request if any non-nullable parameter is omitted.
I'd like to understand whether this is intended behaviour (i.e. conformant to the OData-to-OpenAPI mapping spec as written), and if so whether the spec/library could be tightened so the constraint survives the conversion.
Environment
odata-openapi: 0.29.0 (npm)
- Conversion path: CSDL XML →
xml2json (from odata-csdl) → csdl2openapi → OpenAPI 3.0.2 JSON
- Output target:
openapiVersion: '3.0.2'
Minimal reproduction
Input — example.xml
<?xml version="1.0" encoding="UTF-8"?>
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
<edmx:DataServices>
<Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="Demo">
<EntityType Name="Item">
<Key><PropertyRef Name="ID"/></Key>
<Property Name="ID" Type="Edm.String" Nullable="false"/>
</EntityType>
<Action Name="doSomething" IsBound="true">
<Parameter Name="_it" Type="Demo.Item"/>
<Parameter Name="requiredText" Type="Edm.String" Nullable="false"/>
<Parameter Name="requiredFlag" Type="Edm.Boolean" Nullable="false"/>
<Parameter Name="optionalDate" Type="Edm.Date" Nullable="true"/>
</Action>
<EntityContainer Name="Container">
<EntitySet Name="Items" EntityType="Demo.Item"/>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Conversion script — repro.js
const fs = require('fs');
const { xml2json } = require('odata-csdl/lib/xml2json');
const { csdl2openapi } = require('odata-openapi');
const csdl = xml2json(fs.readFileSync('example.xml', 'utf8'));
const openapi = csdl2openapi(csdl, {
scheme: 'https',
host: 'localhost',
basePath: '/service-root',
openapiVersion: '3.0.2',
});
const action = openapi.paths['/Items/{ID}/Demo.doSomething'].post;
console.log(JSON.stringify(action.requestBody, null, 2));
npm i odata-csdl odata-openapi
node repro.js
Actual output
{
"description": "Action parameters",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"requiredText": { "type": "string" },
"requiredFlag": { "type": "boolean" },
"optionalDate": {
"type": "string", "format": "date",
"example": "2017-04-13",
"nullable": true
}
}
}
}
}
}
requiredText and requiredFlag are correctly emitted as non-nullable (no nullable: true), and optionalDate is correctly marked nullable: true. However the schema has no "required": ["requiredText", "requiredFlag"] array, so per OpenAPI 3.0 semantics every property is optional.
Expected output (if the constraint should survive)
{
"description": "Action parameters",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["requiredText", "requiredFlag"],
"properties": { ... }
}
}
}
}
Where this happens in the code
lib/csdl2openapi.js, function pathItemAction (around line 1632 in 0.29.0):
if (parameters.length > 0) {
const requestProperties = {};
for (const p of parameters) {
requestProperties[p.$Name] = schema(p);
}
pathItem.post.requestBody = {
description: "Action parameters",
content: {
"application/json": {
schema: {
type: "object",
properties: requestProperties,
// no `required: [...]` ever computed
},
},
},
};
}
The per-parameter call to schema(p) correctly translates $Nullable: true to nullable: true (line ~2725 of the same file), but the parameter-level "must be present" constraint never reaches the request body schema.
Why this matters
OData 4.01 Protocol §11.5.5.1 Invoking an Action states:
Non-binding parameters that are nullable or annotated with the term
Core.OptionalParameter [...] MAY be omitted from the request body.
By implication, parameters that are neither nullable nor annotated with
Core.OptionalParameter must be supplied. The gateway in our test service
enforces this and returns 400 Bad Request when a non-nullable, non-optional
parameter is omitted.
With the current OpenAPI output, downstream consumers — including LLM-based
agents that use the OpenAPI doc as a tool spec — produce calls that the
gateway then rejects, with no signal in the contract that those parameters
were required.
For URL-bound function parameters the same library does derive required
from $Nullable (see pathItemFunction), so the asymmetry is purely in the
action requestBody path.
Question on spec conformance
Reading the OData-to-OpenAPI Mapping v1.0:
- §4.5.3 Paths for Action Imports: "Its schema value follows the rules for Schema Objects for complex types, with one property per action parameter." (Example 35 also has no
required array.)
- §4.6.1.1 Schemas for Entity Types and Complex Types: "It also does not contain the
required keyword as response bodies can be arbitrarily tailored using $select, and PATCH request bodies can omit any unaltered properties."
So the spec arguably tells implementations to omit required here, and the library is conformant. But the rationale given for omitting required (response tailoring via $select, partial PATCH bodies) does not apply to an action invocation, which is a POST of a full parameter set. Reusing the complex-type rule for the action requestBody seems like an unintended cross-applicable wording rather than a deliberate decision.
Suggested resolutions (not mutually exclusive)
- Treat the action requestBody as a special case in
pathItemAction / pathItemActionImport and emit required: [...names of non-nullable parameters...]. This is a small, isolated change.
- Alternatively, gate the behaviour behind an option (e.g.
requiredForActionParameters: true) for backwards compatibility.
- Independently raise a clarifying note / erratum on the OData-to-OpenAPI Mapping spec so §4.5.3 explicitly states whether the
required array should be emitted for non-nullable action parameters.
Happy to contribute a PR (option 1 or 2) if the maintainers agree this is desirable.
Summary
When converting a CSDL action with non-nullable parameters (
Nullable="false") to OpenAPI 3.0 viacsdl2openapi, the generatedrequestBodyschema lists every parameter underpropertiesbut does not emit arequiredarray. As a result, downstream OpenAPI consumers (validators, code generators, LLM-based agents, doc UIs) treat all action parameters as optional — even though the OData service will reject the request with400 Bad Requestif any non-nullable parameter is omitted.I'd like to understand whether this is intended behaviour (i.e. conformant to the OData-to-OpenAPI mapping spec as written), and if so whether the spec/library could be tightened so the constraint survives the conversion.
Environment
odata-openapi: 0.29.0 (npm)xml2json(fromodata-csdl) →csdl2openapi→ OpenAPI 3.0.2 JSONopenapiVersion: '3.0.2'Minimal reproduction
Input —
example.xmlConversion script —
repro.jsActual output
{ "description": "Action parameters", "content": { "application/json": { "schema": { "type": "object", "properties": { "requiredText": { "type": "string" }, "requiredFlag": { "type": "boolean" }, "optionalDate": { "type": "string", "format": "date", "example": "2017-04-13", "nullable": true } } } } } }requiredTextandrequiredFlagare correctly emitted as non-nullable (nonullable: true), andoptionalDateis correctly markednullable: true. However the schema has no"required": ["requiredText", "requiredFlag"]array, so per OpenAPI 3.0 semantics every property is optional.Expected output (if the constraint should survive)
{ "description": "Action parameters", "content": { "application/json": { "schema": { "type": "object", "required": ["requiredText", "requiredFlag"], "properties": { ... } } } } }Where this happens in the code
lib/csdl2openapi.js, functionpathItemAction(around line 1632 in 0.29.0):The per-parameter call to
schema(p)correctly translates$Nullable: truetonullable: true(line ~2725 of the same file), but the parameter-level "must be present" constraint never reaches the request body schema.Why this matters
OData 4.01 Protocol §11.5.5.1 Invoking an Action states:
By implication, parameters that are neither nullable nor annotated with
Core.OptionalParametermust be supplied. The gateway in our test serviceenforces this and returns 400 Bad Request when a non-nullable, non-optional
parameter is omitted.
With the current OpenAPI output, downstream consumers — including LLM-based
agents that use the OpenAPI doc as a tool spec — produce calls that the
gateway then rejects, with no signal in the contract that those parameters
were required.
For URL-bound function parameters the same library does derive
requiredfrom
$Nullable(seepathItemFunction), so the asymmetry is purely in theaction requestBody path.
Question on spec conformance
Reading the OData-to-OpenAPI Mapping v1.0:
requiredarray.)requiredkeyword as response bodies can be arbitrarily tailored using$select, and PATCH request bodies can omit any unaltered properties."So the spec arguably tells implementations to omit
requiredhere, and the library is conformant. But the rationale given for omittingrequired(response tailoring via$select, partial PATCH bodies) does not apply to an action invocation, which is a POST of a full parameter set. Reusing the complex-type rule for the action requestBody seems like an unintended cross-applicable wording rather than a deliberate decision.Suggested resolutions (not mutually exclusive)
pathItemAction/pathItemActionImportand emitrequired: [...names of non-nullable parameters...]. This is a small, isolated change.requiredForActionParameters: true) for backwards compatibility.requiredarray should be emitted for non-nullable action parameters.Happy to contribute a PR (option 1 or 2) if the maintainers agree this is desirable.