From 79295927de062e5257bca1dadf83646aabbcacf6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 00:47:42 +0000 Subject: [PATCH 1/3] docs: proposal for OpenAPI import generating response entities (#1234) The OpenAPI importer wires every JSON response as raw `Response: json`; users and agents must hand-write the entities and mapping. Proposes an opt-in `GenerateEntities: Yes` that creates non-persistent entities and writes inline `Response: mapping` (Rest$ImplicitMappingResponseHandling, already supported), phased: P0 stops the silent Swagger 2.0 downgrade (measured: empty contract, no warning) and adds skill guidance; P1 schema model; P2 object-rooted generation; P3 array roots (needs a Studio Pro reference); P4 Swagger 2.0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnpNHdpFqu1sPL7yNAQgTh --- .../PROPOSAL_openapi_response_entities.md | 297 ++++++++++++++++++ docs/11-proposals/README.md | 5 +- 2 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 docs/11-proposals/PROPOSAL_openapi_response_entities.md diff --git a/docs/11-proposals/PROPOSAL_openapi_response_entities.md b/docs/11-proposals/PROPOSAL_openapi_response_entities.md new file mode 100644 index 0000000000..d7501c87da --- /dev/null +++ b/docs/11-proposals/PROPOSAL_openapi_response_entities.md @@ -0,0 +1,297 @@ +--- +title: OpenAPI import — generate response entities and wire operations to them +status: draft +date: 2026-10-21 +related: + - PROPOSAL_mapping_coverage.md +--- + +# Proposal: OpenAPI import generates response entities and mappings + +**Status:** Draft +**Date:** 2026-10-21 +**Issue:** ako/mxcli#1234 (mendixlabs/mxcli#280) + +## Problem Statement + +`create consumed rest service M.Svc ( OpenAPI: '…' )` imports a spec's +operations, parameters and authentication, but not what the operations return. +Every JSON response becomes `Response: json`, which gives a microflow a raw +string. To get objects, the user has to write the non-persistent entities, the +associations and a `Response: mapping` tree for each operation by hand. That is +the part of a REST integration that takes longest and goes wrong most often. + +The report (mendixlabs/mxcli#280) came from the second route into this problem. +An agent asked to "generate the Consume Rest Client from +https://petstore.swagger.io/v2/swagger.json" wrote the entities, JSON +structures and import-mapping documents correctly. It then wired every +operation as `Response: JSON AS $PetJson`, so none of them was used. The tool +gave the agent no way to generate them and no guidance on how to wire them. + +Measured on `main` (d8f0371b9) against both Petstore specs, the gap is larger +than the issue describes: + +| Spec | What `describe contract operation from openapi` produces today | +|---|---| +| OpenAPI 3.0 (`petstore3.swagger.io/api/v3/openapi.json`) | Operations and parameters correct; every response `json`; request bodies `json from $body`; `BaseUrl: ''` with a warning (relative server URL) | +| **Swagger 2.0** (`petstore.swagger.io/v2/swagger.json`, the reporter's spec) | `BaseUrl: ''`, request bodies dropped, **every response `none`**, and **no warning**. The parser reads only OpenAPI 3 fields, so a 2.0 document parses "successfully" into an empty contract | + +The Swagger 2.0 row is a silent downgrade in its own right. It is fixed in +Phase 0 whatever happens to the rest of this proposal. + +**Who benefits:** anyone who consumes a third-party API, and agents most of +all, because generating this scaffolding is exactly what they get wrong. + +## What exists to build on + +The **storage side is already done**. Mendix 11 does not use an import-mapping +document for a consumed REST operation. It stores the mapping **inline** on the +operation as `Rest$ImplicitMappingResponseHandling`, whose `RootMappingElement` +is an `ImportMappings$ObjectMappingElement` tree. mxcli writes and reads that +today (`mdl/backend/modelsdk/consumed_rest_write.go`, +`restImplicitMappingResponseToGen`). `describe` round-trips it (#843), and +`mdl-examples/bug-tests/843-rest-response-mapping.mdl` builds with 0 errors on +11.13.0: + +```sql +operation "SearchRoutes" ( …, + Response: mapping ZZB.Routing { + RoutingCode = routing_code, + RoutingName = routing_name + } +) +``` + +So the issue's framing ("generate JSON structures and import mappings") is the +wrong target for this document type. The target is **non-persistent entities +plus an inline `Response: mapping`**, which no extra documents need to back. + +What does **not** exist: + +| Gap | Where | +|---|---| +| No schema model: `openapi.Schema` is `{type, format}` only. There is no `$ref`, `properties`, `items`, `required`, `enum`, `allOf`, `components.schemas`, or Swagger 2.0 `definitions` | `mdl/openapi/parser.go` | +| `resolveResponseType` reads only `content`, so a 2.0 response (`schema` directly on the response) resolves to `NONE` | `mdl/openapi/parser.go` | +| The inline response writer hardcodes an `(Object)` root with `MaxOccurs 1`. An **array-rooted** response (`findPetsByStatus` returns `Pet[]`) and a nested array (`Pet.tags`) cannot be expressed | `consumed_rest_write.go` | +| Value elements are always written with `DataTypes$StringType`, whatever the attribute's type. This is untested for non-String attributes | `consumed_rest_write.go` | +| Status code fixed at 200 | `consumed_rest_write.go` | + +## BSON Structure + +**Known, and implemented:** `Rest$ImplicitMappingResponseHandling` with +`ContentType`, `StatusCode` and `RootMappingElement` (an +`ImportMappings$ObjectMappingElement` with `Entity`, `ExposedName`, +`JsonPath` such as `(Object)|field`, `ObjectHandling: Create`, `Association`, +`MinOccurs`/`MaxOccurs` and `Children` of +`ImportMappings$ValueMappingElement`). Measured fixes already recorded: +- no `IsDefaultType` on value elements (#1097); +- multi-segment paths (`rest-inline-mapping-paths.mdl`). + +**Unknown. These must be measured from a Studio Pro reference before Phase 2:** + +1. **Array root.** What Studio Pro stores for an operation whose response is a + JSON array: the root element's `JsonPath` (`(Array)`? `(Array)|(Object)`?), + whether there is a wrapper element, and its `MaxOccurs`. Neither the demo + corpus nor mxcli has an example. A related finding recorded zero + `Rest$ImplicitMappingBody` across 9 packages, which suggests the corpus is + thin on inline REST mappings generally. +2. **Nested array** (`Pet.tags: Tag[]`): the element shape and `MaxOccurs -1`, + by analogy with message definitions and JSON-structure mappings. Verify it, + don't assume it. +3. **Value element `Type`** for Integer, Long, Decimal, Boolean and DateTime + attributes: whether `DataTypes$StringType` is accepted, or whether the type + must match the attribute. `mx check` may tolerate a mismatch that fails at + runtime, so this needs a runtime check (see Test Plan). +4. **Association direction and name** that Studio Pro generates when it creates + entities from a JSON response, so generated models match what a Studio Pro + user would get. +5. **Primitive arrays** (`Pet.photoUrls: string[]`): whether Studio Pro maps + them at all (helper entity with a value attribute, as JSON structures do?) + or skips them. + +**Requested reference project:** a Mendix 11.x app with one consumed REST +service holding operations whose responses are: +(a) an object with nested object, object array and primitive array; +(b) a top-level array; +(c) an object with Integer, Long, Decimal, Boolean and DateTime fields. +Entities should be created through Studio Pro's own response-to-entity flow. +`mxcli bson dump --type Rest$ConsumedRestService` on it settles items 1–5. + +## Proposed MDL Syntax + +No new statement. One property on the existing statement, which is off by +default so existing scripts keep their behaviour: + +```sql +create or modify consumed rest service Shop.Petstore ( + OpenAPI: 'https://petstore3.swagger.io/api/v3/openapi.json', + BaseUrl: 'https://petstore3.swagger.io/api/v3', + GenerateEntities: Yes +); +``` + +With `GenerateEntities: Yes`, the statement: + +1. resolves each operation's success response schema (`200`, `201`, then the + first `2xx`) for `application/json`; +2. creates or modifies one **non-persistent entity per named schema** + (`#/components/schemas/Pet` → `Shop.Pet`). Anonymous inline schemas are + named `Response`, or `` when nested; +3. creates an association for each nested object and object array; +4. writes each operation's response as `Response: mapping Shop.Pet { … }`. + +The output is ordinary MDL. `describe contract operation from openapi '…'` +already prints the MDL an import would run, so it gains the entity and +association statements too. The user (or agent) can review the import, or +paste it and edit it instead of running it: + +```sql +create or modify non-persistent entity Shop.Pet ( + Id: Long, + Name: String, + Status: String +); +create or modify non-persistent entity Shop.Category ( Id: Long, Name: String ); +create or modify association Shop.Pet_Category from Shop.Pet to Shop.Category; -- direction: BSON item 4 + +create or modify consumed rest service Shop.Petstore ( … ) +{ + operation getPetById ( Method: get, Path: '/pet/{petId}', Parameters: ($petId: Long), + Response: mapping Shop.Pet { + Id = id, Name = name, Status = status, + create Shop.Pet_Category/Shop.Category = category { Id = id, Name = name } + } + ) +}; +``` + +| OpenAPI | Mendix attribute | +|---|---| +| `integer` / `int32` | Integer | +| `integer` / `int64` | Long | +| `number` | Decimal | +| `boolean` | Boolean | +| `string` / `date-time`, `date` | DateTime | +| `string` (incl. `enum`) | String | +| `$ref` to object | associated entity | +| `array` of object | associated entity, list (BSON item 2) | +| `array` of primitive | BSON item 5 — until measured, reported as not imported | +| `oneOf` / `anyOf` / `additionalProperties` / `not` | not imported, reported by name | +| `allOf` | properties merged into one entity | + +Every schema member the import cannot represent is **listed in the output with +its reason**, never dropped silently. This is the lesson of mendixlabs/mxcli#1118 +and of the Swagger 2.0 row above. + +Attribute names follow the existing rules: Mendix-reserved names are renamed +and reported (`id` → `Id`, `type` → `TypeValue`; CE7247). The mapping keeps +the JSON name, so the wire contract is unaffected. + +## Implementation Plan + +Each phase is its own PR. + +**Phase 0: stop the silent downgrades.** Small, independent, can ship now. +- Detect `swagger: "2.0"` and refuse it with a clear message until Phase 4 + ("Swagger 2.0 is not supported yet; convert with …"), instead of returning an + empty contract. +- Add skill guidance (`.claude/skills/mendix/` REST client skill) to wire + responses with `Response: mapping Entity { … }`, and to remember that an + import-mapping document is not referenced from a consumed REST operation. + This closes the agent route the reporter took. + +**Phase 1: schema model.** Model `Schema` with `$ref` (local `#/` only), +`properties`, `items`, `required`, `enum`, `format`, `allOf`, `nullable`, and +`components.schemas`. Add a resolver with cycle detection: a recursive schema +reuses its entity rather than recursing. Pure unit tests in `mdl/openapi/`. + +**Phase 2: generation for object-rooted responses** (needs BSON items 3–4). +Plan entities, associations and mapping trees from Phase 1. Emit them as **AST +statements executed through the existing handlers** rather than writing +documents directly. Entity creation then goes through the normal create/modify +path, so `canon.Reconcile`, GUID preservation on re-import and validation come +for free, and `describe contract … from openapi` prints the same statements. +Wire `GenerateEntities` through grammar, visitor and AST. + +**Phase 3: array roots and nested arrays** (needs BSON items 1, 2, 5). Extend +`model.RestResponseMapping` and `restInlineMappingElementToGen` with the +measured array shapes, and describe/round-trip them. This is the first change +to the writer. + +**Phase 4: Swagger 2.0.** Normalise 2.0 into the 3.0 model (`definitions` → +`components.schemas`, `host`/`basePath`/`schemes` → server URL, response +`schema` → `content["application/json"]`, `in: body` → request body). Then +remove the Phase 0 refusal. + +**Out of scope:** request bodies as `Body: mapping` (export direction; same +generator, separate PR once responses are proven); non-`2xx` responses; XML +responses; generating enumerations from `enum` (String first, an enumeration +is a later opt-in); external `$ref` URLs. + +### Files to modify/create + +| File | Change | +|------|--------| +| `mdl/openapi/parser.go` | Schema model, `components.schemas`, `$ref` resolver; Swagger 2.0 detection (P0) and normalisation (P4) | +| `mdl/openapi/entities.go` (new) | Schema → planned entities, associations and mapping trees; type table; naming; unsupported-member report | +| `mdl/grammar/MDLParser.g4`, `mdl/visitor/visitor_rest.go`, `restClientSchema` | `GenerateEntities` property | +| `mdl/ast/` | `CreateRestClientStmt.GenerateEntities` | +| `mdl/executor/cmd_rest_clients.go` | `createRestClientFromSpec` and `describeContractFromOpenAPI` run or print the generated statements before the service | +| `model/types.go`, `mdl/backend/modelsdk/consumed_rest_write.go` + read | Array roots and nested arrays (P3), value element types (if item 3 requires it) | +| `.claude/skills/mendix/…` (REST client skill) | P0 guidance | +| `docs-site/src/` REST client reference | `GenerateEntities` and the type table | + +## Version Compatibility + +The OpenAPI import already requires Mendix 11.0+ (`Rest$JsonBody`; see the +`@version: 11.0+` marker in `20-openapi-import-examples.mdl`). +`Rest$ImplicitMappingResponseHandling` is already written under the same gate. +`GenerateEntities` inherits the gate; no new version row is expected. Confirm +against the reference project that array-rooted mappings exist in the +earliest 11.x supported. + +## Test Plan + +- **Unit, `mdl/openapi/`:** `$ref` resolution, `allOf` merge, recursive + schema, unsupported-member report, Swagger 2.0 refusal (P0) and normalisation + (P4). Petstore 2.0 and 3.0 specs as **local fixtures**: the doctype test + today fetches over the network. +- **Executor, `MockBackend`:** `GenerateEntities: Yes` creates the expected + entities and associations and wires each operation. Re-running `create or + modify` creates nothing new and changes no entity's GUID. Include a control + with an entity created by Studio Pro, where `GUID != $ID`, because only that + subject can fail a GUID test (CLAUDE.md). +- **Doctype test:** extend `20-openapi-import-examples.mdl` with + `GenerateEntities: Yes` over the local fixtures. +- **Build, two variants:** `mx check` 0 errors on the generated Petstore 3 + project, and the same project with each unmeasured shape forced in (array + root, a typed value element). The second variant is what shows whether + mxbuild catches a wrong shape at all. +- **Runtime (`verify-in-runtime.md`):** a valid-looking mapping can pass + `mx check` and still fail on the first call. Boot with `run --local` against + a stub server returning Petstore payloads. Assert that a microflow calling + `findPetsByStatus` gets a list of `Pet` with `Category` associated and typed + attributes filled. This is the only test that proves item 3. + +## Open Questions + +1. **BSON items 1–5** above. Can you provide the Studio Pro reference project? + Phase 2 can start on items 3–4 alone; Phase 3 waits for 1, 2 and 5. +2. **What does Studio Pro's own OpenAPI import do with responses?** If it + generates entities, mxcli should copy its naming and shape exactly rather + than invent its own. If it does not, `GenerateEntities` is mxcli's own + addition and the naming below is ours to choose. +3. **Property name and default.** Is `GenerateEntities: Yes` right, and opt-in + rather than default? Opt-in keeps existing scripts from suddenly creating + domain-model entities. A default would make the reporter's one-liner just + work. +4. **Where generated entities go:** always the service's module, or an + `EntitiesModule:`/folder option? Proposed: the service's module, nothing + else until asked. +5. **Name collisions** with an existing entity that the import did not create + (`Shop.Order` already exists and is persistent). Proposed: refuse and name + the conflict, rather than overwrite or suffix. An import must not convert a + user's entity. +6. **Shared schemas across services:** two specs that both declare `Error` in + one module. Proposed: the second import reuses the entity only if its + attributes match; otherwise it refuses as in (5). diff --git a/docs/11-proposals/README.md b/docs/11-proposals/README.md index ac4edf60d3..48d846eb13 100644 --- a/docs/11-proposals/README.md +++ b/docs/11-proposals/README.md @@ -27,7 +27,7 @@ for display in this README): -## Active Proposals (116) +## Active Proposals (117) ### In Progress (partial) (15) @@ -104,7 +104,7 @@ for display in this README): | [VS Code Search — Quick Pick + Workspace Symbol](PROPOSAL_vscode_search.md) | Proposed | Full-text search exists in mxcli (mxcli search) but is only accessible via the terminal. | | [Workflow Improvements: ALTER WORKFLOW + Cross-References](PROPOSAL_workflow_improvements.md) | Proposed | Workflow support in mxcli has full CREATE/DESCRIBE/DROP/SHOW coverage with 13 activity types and BSON round-trip fidelity. | -### Draft (48) +### Draft (49) | Proposal | Status | Summary | |----------|--------|---------| @@ -145,6 +145,7 @@ for display in this README): | [mxcli marketplace diff — detect local modification and plan a GUID-preserving module upgrade](PROPOSAL_marketplace_module_upgrade.md) | Draft | 2026-08-11 (DESCRIBE coverage measured — §7; GUID = database identity measured — §8) | | [mxcli Playground](mxcli-playground.md) | Draft | A public GitHub repository (mendixlabs/mxcli-playground) containing a ready-to-use Mendix project pre-configured with mxcli, Claude Code ski | | [Offline synchronization configuration — the one thing an offline profile needs that MDL cannot say](PROPOSAL_offline_sync_configuration.md) | Draft | mxcli can author an offline navigation profile — PhoneOffline, | +| [OpenAPI import — generate response entities and wire operations to them](PROPOSAL_openapi_response_entities.md) | Draft | Issue: ako/mxcli#1234 (mendixlabs/mxcli#280) | | [Owning the modelsdk/gen codegen — why the vendored generator cannot be adopted as-is](PROPOSAL_codegen_ownership.md) | Draft | Constraint set by the maintainer (2026-08-13): the reflection data is an | | [Playwright Session Reuse and Lifecycle Control](PROPOSAL_playwright_session_reuse.md) | Draft | Builds on proposal-playwright-cli.md, which | | [Project brain — an opt-in store, in a user's Mendix project, for what mxcli cannot compute](PROPOSAL_project_brain.md) | Draft | Audience: users of mxcli building Mendix projects — not mxcli's own | From 9663c246aa5127a1ad1e0b95ba3c01a077b01e85 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 08:57:06 +0000 Subject: [PATCH 2/3] fix: OpenAPI import refuses Swagger 2.0 instead of importing an empty contract The parser reads only OpenAPI 3 fields, so a Swagger 2.0 spec (the reporter's petstore.swagger.io/v2) unmarshalled cleanly into an empty contract: BaseUrl '', request bodies dropped, every response `none`, no warning. It is now refused with a conversion hint. Also documents, in the rest-client skill and the docs-site, that the import returns JSON responses as strings and how to wire `Response: mapping` instead - the route the reporter's agent got wrong. Phase 0 of PROPOSAL_openapi_response_entities.md (ako/mxcli#1234). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnpNHdpFqu1sPL7yNAQgTh --- ...api-swagger2-parses-to-empty-contract.json | 1 + .claude/skills/mendix/rest-client/SKILL.md | 31 ++++++--- docs-site/src/examples/rest-integration.md | 8 ++- docs-site/src/reference/capabilities.md | 2 +- .../280-openapi-swagger2-refused.mdl | 20 ++++++ .../petstore-swagger2.json | 18 +++++ mdl/openapi/parser.go | 14 ++++ mdl/openapi/parser_test.go | 66 +++++++++++++++++++ 8 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 .claude/skills/fix-issue/findings/mdl-other/2026-10-21-openapi-swagger2-parses-to-empty-contract.json create mode 100644 mdl-examples/bug-tests/280-openapi-swagger2-refused.mdl create mode 100644 mdl-examples/openapi-local-specs/petstore-swagger2.json create mode 100644 mdl/openapi/parser_test.go diff --git a/.claude/skills/fix-issue/findings/mdl-other/2026-10-21-openapi-swagger2-parses-to-empty-contract.json b/.claude/skills/fix-issue/findings/mdl-other/2026-10-21-openapi-swagger2-parses-to-empty-contract.json new file mode 100644 index 0000000000..2c72b05471 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-other/2026-10-21-openapi-swagger2-parses-to-empty-contract.json @@ -0,0 +1 @@ +{"area": "mdl/openapi", "date": "2026-10-21", "symptom": "`create consumed rest service … ( OpenAPI: '…swagger.json' )` / `describe contract operation from openapi` on a Swagger 2.0 spec reports success and produces `BaseUrl: ''`, no request bodies and `Response: none` on every operation, with no warning", "cause": "`openapi.ParseSpec` unmarshals into OpenAPI 3 types only; a 2.0 document has none of those fields where they are expected (host/basePath, response `schema`, `in: body`, `definitions`), so unmarshalling succeeds and yields an empty contract", "file": "`mdl/openapi/parser.go` (`Spec.Swagger`, `ParseSpec`)", "insight": "**A permissive unmarshal is a silent format detector that always answers yes**: JSON/YAML into a struct ignores every unknown field, so the wrong document VERSION parses cleanly and degrades into defaults. Read the version discriminator (`swagger` vs `openapi`) before trusting the parse. The reported issue (#280, missing response entities) hid this: the reporter's spec was 2.0, so even response TYPES were lost, which nobody had noticed because the doctype test only ever used the 3.0 Petstore. When testing an importer, run the reporter's actual input before reasoning about the feature gap. Refusal with a conversion hint (swagger2openapi / converter.swagger.io) beats a half-import; full 2.0 normalisation is Phase 4 of PROPOSAL_openapi_response_entities.md. Test `TestParseSpec_RefusesSwagger2` (JSON and YAML) with an OpenAPI 3.0/3.1 control", "refs": ["mendixlabs/mxcli#280", "ako/mxcli#1234"]} diff --git a/.claude/skills/mendix/rest-client/SKILL.md b/.claude/skills/mendix/rest-client/SKILL.md index e13ca14111..b8b367b2ef 100644 --- a/.claude/skills/mendix/rest-client/SKILL.md +++ b/.claude/skills/mendix/rest-client/SKILL.md @@ -11,15 +11,15 @@ description: "Call external REST APIs from Mendix — the three approaches (inli | lines | section | |---|---| | 29-44 | Three Approaches | -| 45-91 | Approach 0: OpenAPI Import (Fastest) | -| 92-288 | Approach 1: REST Client (Manual) | -| 289-334 | Approach 2: REST CALL (Inline HTTP) | -| 335-367 | Data Transformers (JSLT — Mendix 11.9+) | -| 368-405 | JSON Structures & Mappings | -| 406-477 | Complete Pipeline Example | -| 478-508 | Published REST: Authentication Parameters | -| 509-518 | Mendix Validation Rules | -| 519-539 | BSON Types Reference | +| 45-102 | Approach 0: OpenAPI Import (Fastest) | +| 103-299 | Approach 1: REST Client (Manual) | +| 300-345 | Approach 2: REST CALL (Inline HTTP) | +| 346-378 | Data Transformers (JSLT — Mendix 11.9+) | +| 379-416 | JSON Structures & Mappings | +| 417-488 | Complete Pipeline Example | +| 489-519 | Published REST: Authentication Parameters | +| 520-529 | Mendix Validation Rules | +| 530-550 | BSON Types Reference | # REST Integration Skill @@ -44,7 +44,10 @@ No API to call against yet — or one you would rather not depend on while build ## Approach 0: OpenAPI Import (Fastest) -If the API has an OpenAPI 3.0 spec (JSON or YAML), generate the REST client in one command: +If the API has an OpenAPI 3.x spec (JSON or YAML), generate the REST client in one command. +A **Swagger 2.0** spec (`"swagger": "2.0"`, e.g. `petstore.swagger.io/v2/swagger.json`) is +refused — convert it first (`npx swagger2openapi in.json -o out.json`, or +https://converter.swagger.io) and import the converted file: ```sql mdl 1; @@ -87,6 +90,14 @@ describe contract operation from openapi 'specs/capital.json'; **After import:** the REST client is ready to use with `SEND REST REQUEST`. No manual operation definition needed. +**But every JSON response is imported as `response: json` — a raw string, not objects.** The +import does not generate response entities yet (ako/mxcli#1234). To get objects, create the +non-persistent entities yourself and give the operations you call a `response: mapping +Module.Entity { … }` (see *Response Types* below), e.g. by editing the +`describe consumed rest service` output and re-running it as `create or modify`. Writing the +entities and an import-mapping document but leaving the operation at `response: json` is the +common mistake: nothing uses the mapping, and nothing reports that. + --- ## Approach 1: REST Client (Manual) diff --git a/docs-site/src/examples/rest-integration.md b/docs-site/src/examples/rest-integration.md index f8bbe03414..479a07b988 100644 --- a/docs-site/src/examples/rest-integration.md +++ b/docs-site/src/examples/rest-integration.md @@ -219,7 +219,9 @@ CREATE IMPORT MAPPING Integration.IMM_Order ### From an OpenAPI Spec (Recommended) -If the API has an OpenAPI 3.0 spec (JSON or YAML), generate the REST client in one command: +If the API has an OpenAPI 3.x spec (JSON or YAML), generate the REST client in one command. +A Swagger 2.0 spec is refused; convert it to OpenAPI 3 first (for example +`npx swagger2openapi swagger.json -o openapi.json`) and import the converted file. ```sql mdl 1; @@ -236,6 +238,10 @@ CREATE OR MODIFY CONSUMED REST SERVICE PetStoreModule.PetStoreAPI ( Operations, path/query parameters, request bodies, response types, resource groups (from `tags`), and Basic auth are all derived automatically from the spec. +JSON responses are imported as `Response: JSON` (a string). The import does not yet create +entities for them: to get objects, create non-persistent entities and give the operation a +`Response: MAPPING Module.Entity { … }`, as shown under *Manual Definition* below. + **Preview without writing:** ```sql DESCRIBE CONTRACT OPERATION FROM OPENAPI 'specs/capital.json'; diff --git a/docs-site/src/reference/capabilities.md b/docs-site/src/reference/capabilities.md index 78e39865df..34984d86d1 100644 --- a/docs-site/src/reference/capabilities.md +++ b/docs-site/src/reference/capabilities.md @@ -84,7 +84,7 @@ Everything mxcli can do, organized by use case. | Capability | Command | Status | |---|---|---| | REST clients | `CREATE CONSUMED REST SERVICE` | Consumed REST services | -| OpenAPI import | `CREATE CONSUMED REST SERVICE (OpenAPI: '...')` | Generate REST client from OpenAPI 3.0 spec | +| OpenAPI import | `CREATE CONSUMED REST SERVICE (OpenAPI: '...')` | Generate REST client from an OpenAPI 3.x spec (Swagger 2.0 refused; responses imported as JSON, no entities yet) | | Business events | `CREATE BUSINESS EVENT SERVICE` | Event definitions | | Database connections | `CREATE DATABASE CONNECTION` | External SQL | | OData services | `LIST CONSUMED ODATA SERVICES/SERVICES` | Read-only browsing | diff --git a/mdl-examples/bug-tests/280-openapi-swagger2-refused.mdl b/mdl-examples/bug-tests/280-openapi-swagger2-refused.mdl new file mode 100644 index 0000000000..9c015b5717 --- /dev/null +++ b/mdl-examples/bug-tests/280-openapi-swagger2-refused.mdl @@ -0,0 +1,20 @@ +mdl 1; +-- Bug mendixlabs/mxcli#280 (ako/mxcli#1234), Phase 0 of +-- docs/11-proposals/PROPOSAL_openapi_response_entities.md. +-- +-- The reporter imported the Swagger 2.0 Petstore +-- (https://petstore.swagger.io/v2/swagger.json). The OpenAPI importer reads only +-- OpenAPI 3 fields, so a 2.0 document parsed "successfully" into an empty +-- contract: BaseUrl '', request bodies dropped, every response `none`, no warning. +-- +-- It is now refused at execution time. `mxcli check` cannot see that, so this is +-- a plain .mdl. The guard is pinned by TestParseSpec_RefusesSwagger2 +-- (mdl/openapi/parser_test.go). Run it to see the message: +-- +-- mxcli exec mdl-examples/bug-tests/280-openapi-swagger2-refused.mdl +-- +-- Expect: "this is a Swagger 2.0 document; only OpenAPI 3 is supported. Convert +-- it to OpenAPI 3 first ...". Before the fix it printed a service whose every +-- operation had `Response: none`. + +describe contract operation from openapi 'mdl-examples/openapi-local-specs/petstore-swagger2.json'; diff --git a/mdl-examples/openapi-local-specs/petstore-swagger2.json b/mdl-examples/openapi-local-specs/petstore-swagger2.json new file mode 100644 index 0000000000..1822c26c7f --- /dev/null +++ b/mdl-examples/openapi-local-specs/petstore-swagger2.json @@ -0,0 +1,18 @@ +{ + "swagger": "2.0", + "info": {"title": "Swagger Petstore", "version": "1.0.7"}, + "host": "petstore.swagger.io", + "basePath": "/v2", + "schemes": ["https"], + "paths": { + "/pet/{petId}": { + "get": { + "operationId": "getPetById", + "produces": ["application/json"], + "parameters": [{"name": "petId", "in": "path", "required": true, "type": "integer", "format": "int64"}], + "responses": {"200": {"description": "ok", "schema": {"$ref": "#/definitions/Pet"}}} + } + } + }, + "definitions": {"Pet": {"type": "object", "properties": {"id": {"type": "integer"}}}} +} diff --git a/mdl/openapi/parser.go b/mdl/openapi/parser.go index c3062e89f5..f6ae6ef5bc 100644 --- a/mdl/openapi/parser.go +++ b/mdl/openapi/parser.go @@ -19,6 +19,10 @@ import ( // Spec is a minimal OpenAPI 3.0 representation covering fields needed for REST client generation. type Spec struct { + // OpenAPI is the 3.x version string; Swagger is set instead by a 2.0 + // document, which this parser does not read (see ParseSpec). + OpenAPI string `json:"openapi" yaml:"openapi"` + Swagger string `json:"swagger" yaml:"swagger"` Info Info `json:"info" yaml:"info"` Servers []Server `json:"servers" yaml:"servers"` Paths map[string]PathItem `json:"paths" yaml:"paths"` @@ -125,6 +129,16 @@ func ParseSpec(data []byte, ext string) (*Spec, error) { return nil, fmt.Errorf("YAML parse error: %w", err) } } + // A Swagger 2.0 document unmarshals without error into the OpenAPI 3 types, + // but none of the fields that carry its contract (host/basePath, response + // `schema`, `in: body`, `definitions`) exist there. Converting it produced + // an empty contract — BaseUrl '', no request bodies, every response + // `none` — with no warning (mendixlabs/mxcli#280). Refuse it instead. + if spec.Swagger != "" { + return nil, fmt.Errorf("this is a Swagger %s document; only OpenAPI 3 is supported. "+ + "Convert it to OpenAPI 3 first (for example with https://converter.swagger.io "+ + "or `npx swagger2openapi`) and import the converted file", spec.Swagger) + } return &spec, nil } diff --git a/mdl/openapi/parser_test.go b/mdl/openapi/parser_test.go new file mode 100644 index 0000000000..3863c5979e --- /dev/null +++ b/mdl/openapi/parser_test.go @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 + +package openapi + +import ( + "strings" + "testing" +) + +// mendixlabs/mxcli#280 (ako/mxcli#1234) used the Swagger 2.0 Petstore. The +// parser reads only OpenAPI 3 fields, so a 2.0 document parsed "successfully" +// into an empty contract — BaseUrl ”, request bodies dropped, every response +// `none` — and nothing said so. Refusing is strictly better than that. +const swagger2Petstore = `{ + "swagger": "2.0", + "info": {"title": "Swagger Petstore", "version": "1.0.7"}, + "host": "petstore.swagger.io", + "basePath": "/v2", + "schemes": ["https"], + "paths": { + "/pet/{petId}": { + "get": { + "operationId": "getPetById", + "produces": ["application/json"], + "parameters": [{"name": "petId", "in": "path", "required": true, "type": "integer", "format": "int64"}], + "responses": {"200": {"description": "ok", "schema": {"$ref": "#/definitions/Pet"}}} + } + } + }, + "definitions": {"Pet": {"type": "object", "properties": {"id": {"type": "integer"}}}} +}` + +func TestParseSpec_RefusesSwagger2(t *testing.T) { + for _, tc := range []struct{ name, ext, doc string }{ + {"json", ".json", swagger2Petstore}, + {"yaml", ".yaml", "swagger: '2.0'\ninfo:\n title: T\n version: '1'\npaths: {}\n"}, + } { + t.Run(tc.name, func(t *testing.T) { + spec, err := ParseSpec([]byte(tc.doc), tc.ext) + if err == nil { + t.Fatalf("a Swagger 2.0 document was accepted (%d paths) — it converts into an empty contract with no warning", len(spec.Paths)) + } + for _, want := range []string{"Swagger 2.0", "OpenAPI 3"} { + if !strings.Contains(err.Error(), want) { + t.Errorf("error should name %q so the user knows what to do: %v", want, err) + } + } + }) + } +} + +// CONTROL: OpenAPI 3.0 and 3.1 still parse. +func TestParseSpec_AcceptsOpenAPI3(t *testing.T) { + for _, v := range []string{"3.0.2", "3.1.0"} { + doc := `{"openapi": "` + v + `", "info": {"title": "T", "version": "1"}, "servers": [{"url": "https://x.example/api"}], + "paths": {"/a": {"get": {"operationId": "getA", "responses": {"200": {"description": "ok", "content": {"application/json": {}}}}}}}}` + spec, err := ParseSpec([]byte(doc), ".json") + if err != nil { + t.Fatalf("OpenAPI %s refused: %v", v, err) + } + svc, _, err := ToRestClientModel(spec, "Svc", "") + if err != nil || len(svc.Operations) != 1 || svc.Operations[0].ResponseType != "JSON" { + t.Errorf("OpenAPI %s did not convert to one JSON operation: %+v, %v", v, svc, err) + } + } +} From 72b8a71b23eac1d08208f93395ed444b4673d84f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 19:56:00 +0000 Subject: [PATCH 3/3] docs(skill): give the rest-client alter example its mdl 1; header TestSkillsTeachMDL1 requires every complete script in a skill to start with `mdl 1;`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnpNHdpFqu1sPL7yNAQgTh --- .claude/skills/mendix/rest-client/SKILL.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.claude/skills/mendix/rest-client/SKILL.md b/.claude/skills/mendix/rest-client/SKILL.md index bec6a8746d..d5525a77da 100644 --- a/.claude/skills/mendix/rest-client/SKILL.md +++ b/.claude/skills/mendix/rest-client/SKILL.md @@ -11,15 +11,15 @@ description: "Call external REST APIs from Mendix — the three approaches (inli | lines | section | |---|---| | 29-44 | Three Approaches | -| 45-109 | Approach 0: OpenAPI Import (Fastest) | -| 110-336 | Approach 1: REST Client (Manual) | -| 337-382 | Approach 2: REST CALL (Inline HTTP) | -| 383-415 | Data Transformers (JSLT — Mendix 11.9+) | -| 416-453 | JSON Structures & Mappings | -| 454-525 | Complete Pipeline Example | -| 526-556 | Published REST: Authentication Parameters | -| 557-566 | Mendix Validation Rules | -| 567-587 | BSON Types Reference | +| 45-110 | Approach 0: OpenAPI Import (Fastest) | +| 111-337 | Approach 1: REST Client (Manual) | +| 338-383 | Approach 2: REST CALL (Inline HTTP) | +| 384-416 | Data Transformers (JSLT — Mendix 11.9+) | +| 417-454 | JSON Structures & Mappings | +| 455-526 | Complete Pipeline Example | +| 527-557 | Published REST: Authentication Parameters | +| 558-567 | Mendix Validation Rules | +| 568-588 | BSON Types Reference | # REST Integration Skill @@ -96,6 +96,7 @@ non-persistent entities yourself and give the operations you call a mapping (see Types* below): ```sql +mdl 1; alter consumed rest service PetStoreModule.PetStoreAPI modify operation getPetById ( response: mapping PetStoreModule.Pet { Id = id, Name = name }