Skip to content

Action parameter Nullable="false" lost in conversion: requestBody schema has no required array #354

Description

@axherrm

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)

  1. 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.
  2. Alternatively, gate the behaviour behind an option (e.g. requiredForActionParameters: true) for backwards compatibility.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Review

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions