Skip to content

fix(mappings): import-mapping Range authoring (#881) and JSON member-name resolution (#882) - #140

Merged
ako merged 4 commits into
mainfrom
claude/mxcli-issues-ovfoxk
Aug 13, 2026
Merged

fix(mappings): import-mapping Range authoring (#881) and JSON member-name resolution (#882)#140
ako merged 4 commits into
mainfrom
claude/mxcli-issues-ovfoxk

Conversation

@ako

@ako ako commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Two related mapping fixes. Both were reported as one thing and turned out to be another; both are verified against mxbuild with the reported symptom reproduced and then removed.


Part 1 — mendixlabs#881: author and round-trip the import-mapping Range

Mendix stores the Range on the ImportMappingCall in two variants:

Microflows$ConstantRange{SingleObject}                     All / First
Microflows$CustomRange{LimitExpression, OffsetExpression}  Custom

mxcli only ever wrote the first, and read only SingleObject. So "Custom" was unrepresentable, not merely undescribed — a bounded import became unbounded the moment anything rewrote the activity — and DESCRIBE emitted no range at all, so all three settings round-tripped identically and describe → edit → exec silently changed the activity's meaning.

Syntax

import from mapping M.IMM($src) [all | first | limit <expr> [offset <expr>]]

Omitting the clause keeps the pre-existing inference from the mapping's root shape, so scripts that predate the syntax write exactly what they always did.

DESCRIBE always emits one of the three, never nothing: silence re-enters that inference, and an object-rooted mapping set to All — Studio Pro's own default, shipped in the blank app — comes back as First.

limit/offset mirror the retrieve clause rather than inventing a second spelling. first is a separate word on purpose: limit 1 is a list of one, first binds a single object, so they cannot share syntax.

The trap in the fix

The Range and the result variable's cardinality are separate axes. The first pass folded them, and every all wrote a ListType against an object-rooted mapping — which mxbuild rejects with CE0243 ("the mapping used to return 'List of X' but now returns 'X'").

Mendix's own FeedbackModule.SUB_Feedback_PostToAppInsights settles it: ConstantRange{SingleObject:false} bound to an ObjectType variable, because its mapping returns one object. So the stored VariableType is the authority on cardinality, the range is its own flag (RangeSingleObject *bool, nil = follow the inference), and only first pins both.

Second trap: the ImportMappingCall is built at three sites — importXmlActionToGen, the REST ResultHandlingMapping case, and the legacy writer. Patching two let a limit reach the model while a ConstantRange was still written. Each engine now has one shared range helper.

Known limitation, documented rather than validated

Mendix rejects offset with CE6100 ("This entity does not support offset") unless the mapping's root is a list; limit alone is accepted either way. I could not add a checker, because mxcli cannot currently author a list-rooted import mapping — array-root mappings emit CE5015, a separate defect — so there is no positive case to test a rule against. The constraint is documented in the skill, the quick reference, the microflow.mapping syntax topic, and the example file instead.


Part 2 — mendixlabs#882: resolve JSON members by either name, stop inventing paths

The reported cause is Mendix's own convention

A JsonStructures$JsonElement carries two names, and for any lowercase-initial key they differ:

Path         (Object)|uuid    the raw JSON key — what the RUNTIME resolves by
ExposedName  Uuid             Mendix's derived name — what Studio Pro DISPLAYS

Both are Mendix's. A blank app's Studio-Pro-authored FeedbackModule.JSON_AppInsightsResponse stores ExposedName "Uuid" against Path "(Object)|uuid", and its IMM_PostResponse binds JsonPath "(Object)|uuid". So the capitalisation DESCRIBE shows (totalTotal, __Value__ValueItem) is faithful rendering, not corruption. Running the reporter's repro as written confirms it: the stored paths come out byte-identical to the structure's.

The actual defect

DESCRIBE emits the exposed name; the builder resolved only raw keys. mxcli's own output therefore did not round-trip — re-executing a DESCRIBE fabricated a path:

(Object)|total                             →  (Object)|Total
(Object)|entityInstances|__Value|(Object)  →  (Object)|EntityInstances|__ValueItem

The array's |(Object) item marker vanished entirely and MaxOccurs went to 0. mxcli check passed; mxbuild reported CE5015. Export mappings had the identical bug.

Members now resolve by raw key or exposed name — including an array addressed by its item's exposed name, which resolves to the array so the |(Object) step is still taken. A member matching neither is refused, listing the spellings that would have worked:

Error: import mapping B.IM: "camelCse" is not a member of the JSON structure at
(Object); available: total (or Total), camelCase (or CamelCase),
entityInstances (or EntityInstances)

An invented path passed mxcli check and surfaced only later — the worst failure mode, because the tool that wrote it reported success.

Two divergences from what Studio Pro writes

IsDefaultType on value elements. generated/metamodel declares it on Import/ExportObjectMappingElement and on neither value type, and Studio Pro's own mappings carry it on the object element alone. Per the overlay-writes rule in CLAUDE.md, a property the type does not own is the shape mxbuild tolerates and Studio Pro refuses to open — so a green build is not evidence. Dropped from the value writers in both engines.

OriginalValue carrying the snippet's sample. It is the sample value parsed out of the JSON structure's snippet ("42", "\"Widget\"") and belongs to the structure. Measured across both Studio-Pro-authored mappings a blank app ships (IMM_PostResponse and EMM_PostFeedback, ~15 value elements): all "", while their structures carry 17 non-empty samples. No longer cloned.

What this does not fix

The reporter's runtime error — key not found: Path(QName(None,),None,) at MappingCache.storeValueMappingElement — is not reproduced. Their standalone repro, executed by mxcli and run against a real Mendix runtime, imports cleanly on both 11.6.6 and 11.13.0, bracketing their 11.12.1. Their mx check is also clean, which rules out the fabricated-path defect above. Something their real endpoint carries and the repro does not is still unaccounted for.


Verification

Against mxbuild, on both engines (modelsdk and legacy):

Check Result
mx check, all fixtures 0 errors (was CE5015 on the round-trip case)
mendixlabs#881: three ranges re-describe verbatim exact
mendixlabs#882: raw-key vs exposed-name spelling byte-identical stored paths, import and export
Reporter's mendixlabs#882 repro at runtime, 11.6.6 / 11.13.0 PASS / PASS

Every fix was reverted or stubbed in turn to confirm the reported symptom returns: re-conflating the Range axes reproduces CE0243; stubbing the $Type dispatch drops the limit; stubbing the exposed-name lookup reproduces the fabricated path; re-adding IsDefaultType fails its test.

One methodology note worth recording: an early runtime run appeared to reproduce a mapping failure, and the control — Studio Pro's own mapping in the same harness — failed identically, locating the fault in the test's missing module-role grants rather than in anything mxcli wrote. Without that control this PR would have claimed a false reproduction.

Changes

Layer Files
Grammar MDLMicroflow.g4 (importMappingRange), MDLLexer.g4 (FIRST), MDLSettings.g4 (keyword list)
AST / visitor mdl/ast/ast_microflow.go, mdl/visitor/visitor_import_export_mapping.go
Semantic model sdk/microflows/microflows_actions.go (RangeSingleObject, LimitExpression, OffsetExpression, RangeSingleObjectOf)
Executor — mendixlabs#881 cmd_microflows_builder_calls.go, cmd_microflows_format_action.go
Executor — mendixlabs#882 cmd_import_mappings.go (jsonSchemaIndex, resolve, memberNames), cmd_export_mappings.go
modelsdk engine microflow_write.go, microflow_read_actions.go, mapping_write.go
Legacy engine sdk/mpr/writer_microflow_actions.go, parser_microflow_actions.go, writer_import_mapping.go, writer_export_mapping.go
Docs MDL_QUICK_REFERENCE.md, json-structures-and-mappings.md skill, syntax/features_microflow.go (new microflow.mapping topic), fix-issue.md symptom rows
Tests cmd_microflows_import_range_test.go, microflow_import_range_test.go, parser_microflow_import_range_test.go, cmd_mappings_member_resolution_test.go, mapping_isdefaulttype_test.go
Examples mdl-examples/bug-tests/881-import-mapping-range.mdl, 882-mapping-member-names.mdl

One drive-by in the legacy parser: it read VariableType off the ImportMappingCall rather than the ResultHandling, so the result entity came back empty for everything mxcli or Studio Pro writes.

claude added 3 commits August 13, 2026 14:41
Mendix stores the import activity's Range on the ImportMappingCall in two
variants:

  Microflows$ConstantRange{SingleObject}                     All / First
  Microflows$CustomRange{LimitExpression, OffsetExpression}  Custom

mxcli only ever wrote the first and read only SingleObject, so "Custom" was
not merely undescribed but unrepresentable: a bounded import became unbounded
the moment anything rewrote the activity. DESCRIBE emitted no range at all,
so all three settings round-tripped identically and describe -> edit -> exec
silently changed the activity's meaning.

Adds the trailing clause:

  import from mapping M.IMM($src) [all | first | limit <e> [offset <e>]]

Omitting it keeps the pre-existing inference from the mapping's root shape,
so scripts that predate the syntax write exactly what they always did.
DESCRIBE always emits one of the three, because silence re-enters that
inference and an object-rooted mapping set to All -- Studio Pro's own default,
shipped in the blank app -- comes back as First.

The Range and the result variable's cardinality are separate axes. Folding
them writes a ListType against an object-rooted mapping, which mxbuild rejects
with CE0243; Mendix's own FeedbackModule.SUB_Feedback_PostToAppInsights pairs
ConstantRange{SingleObject:false} with an ObjectType variable. The stored
VariableType is the authority on cardinality, the range is its own flag
(RangeSingleObject, nil = follow the inference), and only `first` pins both.

The ImportMappingCall is built at three sites -- the import statement, REST
result handling, and the legacy writer -- so the range selection is one shared
helper per engine; fixing two of the three let a limit reach the model while a
ConstantRange was still written. Both engines are fixed: they share the
semantic model, and a fix in one is invisible to a user on the other.

Verified end-to-end on mxbuild 11.6.6 (mx check: 0 errors on both engines,
all three ranges re-describing verbatim). `offset` is rejected by Mendix with
CE6100 unless the mapping's root is a list, while `limit` is accepted either
way; that is documented rather than validated, because mxcli cannot currently
author a list-rooted import mapping to test the positive case against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
…g paths

A JsonStructures$JsonElement carries TWO names, and for any lowercase-initial
key they differ:

  Path         "(Object)|uuid"   the raw JSON key — what the RUNTIME resolves by
  ExposedName  "Uuid"            Mendix's derived name — what Studio Pro DISPLAYS

Mendix derives the exposed name by capitalising the initial, and by suffixing
"Item" on an array's item object. Both are Mendix's own: the blank app's
Studio-Pro-authored FeedbackModule.JSON_AppInsightsResponse stores ExposedName
"Uuid" against Path "(Object)|uuid", and its IMM_PostResponse binds JsonPath
"(Object)|uuid". So the capitalisation DESCRIBE shows is faithful rendering.

The defect is that mxcli's DESCRIBE emits the exposed name while its builder
resolved only raw keys, so mxcli's own output did not round-trip. Re-executing a
DESCRIBE fabricated a path from the exposed name:

  (Object)|total                             ->  (Object)|Total
  (Object)|entityInstances|__Value|(Object)  ->  (Object)|EntityInstances|__ValueItem

The array's "|(Object)" item marker vanished entirely and MaxOccurs went to 0.
`mxcli check` passed; mxbuild reported CE5015. Export mappings had the identical
bug.

Members now resolve by raw key or exposed name — including an array addressed by
its item's exposed name, which resolves to the array so the "|(Object)" step is
still taken. A member matching neither is REFUSED, listing the spellings that
would have worked, instead of being written with an invented path: that path
passed `mxcli check` and surfaced only later, which is the worst failure mode
because the tool that wrote it reported success.

Separately, both engines wrote IsDefaultType on every ValueMappingElement. The
generated metamodel declares it on Import/ExportObjectMappingElement and on
neither value type, and Studio Pro's own mappings carry it on the object element
alone. Per the overlay-writes rule in CLAUDE.md, a property the type does not own
is the shape mxbuild tolerates and Studio Pro refuses to open, so a green build
is not evidence — it is dropped from the value writers in both engines.

Verified on mxbuild 11.6.6, both engines: the raw-key and exposed-name spellings
now produce byte-identical stored paths, and `mx check` reports 0 errors where it
previously reported CE5015. Each fix was reverted in turn to confirm the symptom
returns.

Refs upstream mendixlabs#882.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
…ing elements

A JSON structure element's OriginalValue is the SAMPLE parsed out of the snippet
("42", "\"Widget\""). It describes the structure, not the mapping, and Studio Pro
leaves it empty on every mapping element.

Measured across the two Studio-Pro-authored mappings a blank app ships —
FeedbackModule.IMM_PostResponse and EMM_PostFeedback, ~15 value elements between
them — all write OriginalValue "", while their JSON structures carry 17 non-empty
samples. mxcli cloned the sample in, so an mxcli-written mapping differed from a
Studio-Pro-written one over the same structure by the snippet's example data,
which is the first thing a side-by-side comparison of the two shows.

Refs upstream mendixlabs#882.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
@ako ako changed the title fix(microflow): author and round-trip the import-mapping Range fix(mappings): import-mapping Range authoring (#881) and JSON member-name resolution (#882) Aug 13, 2026
The refusal added for mendixlabs#882 fired whenever a member failed to resolve, including
when there was nothing to resolve against. `create import mapping X { ... }` with
no `with json structure` clause is legal MDL, and an XML-schema or
message-definition mapping resolves no JSON elements either — so every
schema-less mapping was rejected:

  "id" is not a member of the JSON structure at (Object),
  which has no members there

That broke eight round-trip integration tests, which unit tests did not cover
because they all build against a populated index.

The refusal now applies only where a schema exists to contradict the name. With
no schema loaded, the authored name is taken at face value and becomes both the
exposed name and the path segment — the pre-mendixlabs#882 behaviour, unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uQvFDd5R4eNqqita59jM8
@ako
ako merged commit bd1835d into main Aug 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants