JSON Structures, Import Mappings & Export Mappings

SkillAI & models

Lets your agent create and manage JSON structures and import/export mappings for a domain model.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the JSON Structures, Import Mappings & Export Mappings skill

About this capability

Create and manage JSON structures, import mappings and export mappings in MDL, plus the domain-model shapes they map onto. Use when turning a JSON payload into entities, mapping a REST or queue response, or exporting objects as JSON.

What this skill tells your AI

The instructions your AI receives, as published by mendixlabs/mxcli in .claude/skills/mendix/json-structures-and-mappings/SKILL.md and read by ahel’s review.

This skill covers creating and managing JSON structures, import mappings, and export mappings in Mendix using MDL.

Key Concepts

JSON Structures

A JSON structure defines the schema of a JSON payload. It stores a JSON snippet and auto-derives an element tree with paths, types, and custom names.

Import Mappings

An import mapping converts a JSON string into Mendix entity objects. It maps JSON fields to entity attributes.

Two names per member: the raw key and the exposed name

Every JSON structure element stores both, and for any lowercase-initial key they differ:

ExampleUsed for
Path (raw JSON key)(Object)|uuidwhat the runtime resolves by
ExposedName (derived)Uuidwhat Studio Pro displays

Mendix derives the exposed name by capitalising the initial, and for an array's item object by suffixing Item — so total → Total, camelCase → CamelCase, __Value (array) → __ValueItem (its item). Keys already starting with an underscore are left alone: __returnedCount stays __returnedCount.

This is Mendix's own convention, not something mxcli does. 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".

Consequences worth knowing:

  • Either spelling works in MDL. Total = total and Total = Total produce the same stored mapping. Write whichever you have.
  • DESCRIBE emits the raw JSON key, so its output reproduces the script that produced the mapping — Total = total comes back as Total = total, and an array binding as = item rather than = ItemItem. It also emits create or modify, so the output re-runs against the project it was read from. (Until #915 it printed the exposed name and a bare create: the text differed from the input, making every script-vs-describe diff noise, and re-running it failed with "import mapping already exists". The stored mapping was correct either way.)
  • A member matching neither spelling is refused, listing what would have worked. It is never written with a guessed path: such a mapping passed mxcli check and failed later in mxbuild (CE5015) or at runtime.
Inherited attributes

Mendix inheritance is multi-table: all of a parent's attributes are members of the child, so an entity created with extends can map them. Name an inherited attribute exactly like one of the entity's own — mxcli resolves each to the entity that declares it, which is the reference Studio Pro needs to show the field mapped.

create persistent entity Docs.DocumentBase (
  DocName: String(200),
  Confidential: Boolean
);

create persistent entity Docs.Contract extends Docs.DocumentBase (
  ContractNumber: String(50)
);

create import mapping Docs.IMM_Contract
  with json structure Docs.JSON_Contract
{
  create Docs.Contract {
    ContractNumber = contractNumber,   -- own
    DocName        = docName,          -- inherited
    Confidential   = confidential      -- inherited
  }
};

Qualifying an inherited attribute against the entity being mapped instead of its declaring entity is Mendix CE1613 "The selected attribute ... no longer exists", and the field shows unmapped in Studio Pro.

Export Mappings

An export mapping converts Mendix entity objects into a JSON string. It maps entity attributes to JSON fields.

Critical: Import and Export Need Different Domain Models

Import and export mappings for the same JSON structure typically require different entity structures.

  • Import: The child entity owns the FK to the parent (from Child to Parent). Arrays map directly to the item entity — no intermediate container entity needed.
  • Export: The domain model mirrors the JSON structure. Arrays need an intermediate container entity (e.g., Items) plus an item entity (e.g., ItemsItem). The container links to the parent, the item links to the container.

JSON Structures

Create

create json structure Module.JSON_Pet
  snippet '{"id": 1, "name": "Fido", "status": "available"}';

For multi-line JSON, use dollar-quoting:

create json structure Module.JSON_Order
  snippet $${
  "orderId": 100,
  "customer": {"name": "Alice", "email": "alice@example.com"},
  "items": [{"sku": "A1", "quantity": 2, "price": 9.99}]
}$$;

Custom name mapping (rename JSON fields):

create json structure Module.JSON_Pet
  snippet '{"id": 1, "name": "Fido"}'
  CUSTOM NAME map ('id' as '_id');

Name array items yourself — item of (ako/mxcli#272). An array's item is the anonymous [...] entry, so it has no JSON key and the plain form cannot reach it; left alone it gets a derived name like LinesItem:

create json structure Module.JSON_Invoice
  snippet '{"lines": [{"sku": "A1"}], "tags": ["urgent"]}'
  CUSTOM NAME map (
    'lines' as 'OrderLines',
    item of 'lines' as 'OrderLine',
    item of 'tags' as 'Tag'
  );

This is worth doing rather than accepting the default: a mapping element clones the schema element's ExposedName, so the item's name is what every mapping over this structure carries, and it is one of the two names a member resolves by.

  • The two clauses are independent — naming an item does not require renaming its array, so adding one is a one-line diff.
  • item of names a primitive array's wrapper too; that wrapper is the item.
  • A root-level array has no key: item of 'Root' as 'Entry'.
  • An entry whose key is not in the snippet is an error (MDL-JSON01), as is item of on something that is not an array (MDL-JSON02).

Browse

show json structures;
show json structures in module;
describe json structure Module.JSON_Pet;
drop json structure Module.JSON_Pet;

Message Definitions

A mapping's source can also be a message definition — 74 of the 327 mappings in the demo corpus (22.6%), and the only non-JSON source MDL can create. It holds nothing external: it is a selection over the domain model.

create message definition collection Sales.MD_Order (
  definition OrderMessage for Sales.Order as 'Orders' (
    OrderId,
    Sales.Order_Customer/Sales.Customer ( FirstName )
  )
);

A bare name is an attribute; Assoc/Module.Entity is an association. Name the target entity — the stored cardinality follows the direction of traversal and the association's type, so a Reference gives a single object one way and a list the other, while a ReferenceSet is a list both ways.

The full vocabulary, the ALTER statements, inherited attributes and what mxcli deliberately does not guess: reference/message-definitions.md.

Import Mappings

Domain Model for Import

For import mappings, associations point FROM the child entity TO the parent:

create non-persistent entity Module.OrderResponse (
  OrderId: integer
);
/

create non-persistent entity Module.CustomerInfo (
  Name: string,
  Email: string
);
/

create non-persistent entity Module.OrderItem (
  Sku: string,
  Quantity: integer,
  Price: decimal
);
/

-- Child entity owns the FK (FROM child TO parent)
create association Module.CustomerInfo_OrderResponse
  from Module.CustomerInfo
  to Module.OrderResponse;
/

create association Module.OrderItem_OrderResponse
  from Module.OrderItem
  to Module.OrderResponse;
/

Simple Import Mapping (flat JSON)

create import mapping Module.IMM_Pet
  with json structure Module.JSON_Pet
{
  create Module.PetResponse {
    PetId = id,
    Name = name,
    status = status
  }
};

Nested Import Mapping (objects and arrays)

Arrays map directly to the item entity — no intermediate container needed:

create import mapping Module.IMM_Order
  with json structure Module.JSON_Order
{
  create Module.OrderResponse {
    OrderId = orderId,
    create Module.CustomerInfo_OrderResponse/Module.CustomerInfo = customer {
      Name = name,
      Email = email
    },
    create Module.OrderItem_OrderResponse/Module.OrderItem = items {
      Sku = sku,
      Quantity = quantity,
      Price = price
    }
  }
};

Object Handling

Mendix stores two properties here, not one: how to get the object, and what to do when a find comes up empty. Both are yours to choose.

SyntaxMeaning
create Module.EntityAlways create a new object (default)
find Module.Entity or createFind by KEY, create one if not found
find Module.Entity or errorFind by KEY, fail the import if not found
find Module.Entity or ignoreFind by KEY, skip the element if not found
find or create Module.EntityThe older spelling of find … or create

Append overridable to let the caller override the backup at import time: find Module.PetResponse or create overridable.

create import mapping Module.IMM_UpsertPet
  with json structure Module.JSON_Pet
{
  find Module.PetResponse or create {
    PetId = id key,
    Name = name,
    status = status
  }
};

A bare find is refused. Which of the three you get is a real runtime difference, and mxcli used to pick create for you whatever the document said — so it now asks rather than guessing.

A find has two requirements, and mxcli check enforces both (ako/mxcli#253):

  1. At least one member marked key, per searching element — nested ones included. Without it there is nothing to search on: CE0250, reported as MDL-MAP02. (key is only valid with find; on a create it means nothing.)
  2. A persistable entity. A search is a database query, and a non-persistent entity has no database: CE0251, reported as MDL-MAP03. Persistability comes from the generalization chain, not the entity's own flag — an entity declared with plain create entity that extends a non-persistent parent is still not searchable.

Re-measuring these is easy to get wrong: mxbuild reports one at a time. A keyless find over a non-persistent entity is CE0250 only, and CE0251 appears only once a key exists.

A custom handler is exempt from both — the microflow is the find, so there is no key to declare and no query to run.

Custom Object Handling and the Mapping's Input Object

A microflow can resolve the object instead of Create/Find. Write it as by on the element; the microflow's parameters are named with their sources:

SourceMeans
parentthe enclosing mapped object
parameterthe mapping's own input object
parent(2)an ancestor N levels up
a/b/ca value from the payload, addressed like any other member

parameter needs the mapping to declare an input object, which is a clause on the header — import mappings only:

create import mapping Module.IMM_Embed
  with json structure Module.JSON_Embed
  parameter GenAICommons.ChunkCollection
{
  create GenAICommons.ChunkCollection {
    Name = id,
    find Module.Chunk_ChunkCollection/GenAICommons.Chunk
      by Module.MF_FindChunk ( Collection: parameter, Index: idx )
      = embeddings {
        Text = text
      }
  }
};

Using parameter without declaring one is refused — the build reports it as CE0279. The declared entity must match the microflow's parameter type, which the build checks as CE0282.


Export Mappings

Domain Model for Export

Export mappings require entities that mirror the JSON structure. Arrays need an intermediate container entity:

-- Root entity (matches top-level JSON object)
create non-persistent entity Module.ExRoot (
  OrderId: integer
);
/

-- Nested object entity (1-1 relationship, use OWNER Both)
create non-persistent entity Module.ExCustomer (
  Name: string,
  Email: string
);
/

-- Array CONTAINER entity (no attributes, just links parent to items)
create non-persistent entity Module.ExItems;
/

-- Array ITEM entity (attributes for each array element)
create non-persistent entity Module.ExItemsItem (
  Sku: string,
  Quantity: integer,
  Price: decimal
);
/

-- Associations: child FROM, parent TO
create association Module.ExCustomer_ExRoot
  from Module.ExCustomer
  to Module.ExRoot
  owner both;   -- 1-1 for nested objects
/

create association Module.ExItems_ExRoot
  from Module.ExItems
  to Module.ExRoot;   -- 1-* for arrays
/

create association Module.ExItemsItem_ExItems
  from Module.ExItemsItem
  to Module.ExItems;   -- 1-* for array items
/

Simple Export Mapping (flat JSON)

create export mapping Module.EMM_Pet
  with json structure Module.JSON_Pet
{
  Module.PetResponse {
    id = PetId,
    name = Name,
    status = status
  }
};

Nested Export Mapping (objects and arrays)

Arrays have TWO levels: container entity + item entity:

create export mapping Module.EMM_Order
  with json structure Module.JSON_Order
{
  Module.ExRoot {
    orderId = OrderId,
    Module.ExCustomer_ExRoot/Module.ExCustomer as customer {
      name = Name,
      email = Email
    },
    Module.ExItems_ExRoot/Module.ExItems as items {
      Module.ExItemsItem_ExItems/Module.ExItemsItem as ItemsItem {
        sku = Sku,
        quantity = Quantity,
        price = Price
      }
    }
  }
};

NULL VALUES option

create export mapping Module.EMM_Pet
  with json structure Module.JSON_Pet
  null values SendAsNil     -- or LeaveOutElement (default)
{
  ...
};

Starting a Mapping Below the Payload Root

A mapping does not have to start at the top of the JSON. root a/b/c on the source clause selects the element it starts at, and the path may pass through arrays — the mapping is then rooted at the array's item, so it yields one object per entry.

create import mapping RootDemo.IMM_Choices
  with json structure RootDemo.JSON_Completion root response/choices/message
{ create RootDemo.Message { Role = role, Content = content } };

Worked examples, the array-crossing rule and what it does to a call's cardinality: reference/mapping-root-selection.md.

Microflow Actions

Import from Mapping (JSON → entities)

-- With result variable (non-persistent entities)
$PetResponse = import from mapping Module.IMM_Pet($JsonContent);

-- Without result variable (persistent entities, stores to DB)
import from mapping Module.IMM_Pet($JsonContent);
Range — how much of the result to bind

Optional trailing clause, matching Studio Pro's All / First / Custom setting on the activity. Omit it and mxcli infers from the mapping's own root shape, as it always has.

$Pets = import from mapping Module.IMM_Pets($Json) all;            -- All (the default)
$Pet  = import from mapping Module.IMM_Pets($Json) first;          -- First: ONE object
$Page = import from mapping Module.IMM_Pets($Json) limit 10;       -- Custom
$Page = import from mapping Module.IMM_Pets($Json) limit 10 offset 5;

first is a separate word from limit 1 on purpose: limit 1 is a list of one, first binds a single object, so the result variable's type differs.

Two things the range does not do:

  • It does not change what the mapping returns. An object-rooted mapping binds an object under every range — all on one is Studio Pro's own default, and the blank app ships one (FeedbackModule.SUB_Feedback_PostToAppInsights). Only first narrows a list mapping to a single object.
  • offset is not accepted everywhere. Mendix rejects it with CE6100 ("This entity does not support offset") unless the mapping's root is a list; limit alone is fine either way. Verified on mxbuild 11.6.6.

Export to Mapping (entity → JSON)

$JsonOutput = export to mapping Module.EMM_Pet($PetResponse);

Complete Pipeline

create microflow Module.ProcessData ()
begin
  declare $json string = $latestHttpResponse/content;
  $PetResponse = import from mapping Module.IMM_Pet($json);
  -- Process...
  $Output = export to mapping Module.EMM_Pet($PetResponse);
  log info node 'Integration' 'Result: ' + $Output;
end;
/

Browse

show import mappings [in module];
show export mappings [in module];
describe import mapping Module.Name;
describe export mapping Module.Name;
drop import mapping Module.Name;
drop export mapping Module.Name;

Export Workflow: PE → NPE → JSON

Export mappings work on non-persistent entity (NPE) structures that mirror the target JSON. When the source data is in persistent entities (PE) in the database, the typical workflow is:

  1. Retrieve persistent data from the database
  2. Build NPE tree in a microflow: create NPE objects, set attributes, link via associations to match the JSON structure
  3. Export to mapping to serialize the NPE tree to JSON
-- Example: build NPE tree from persistent Order data, then export
create microflow Module.ExportOrder ($Order: Module.Order)
returns string as $json
begin
  -- Build the NPE tree matching the JSON structure
  $Root = create Module.ExRoot (OrderId = $Order/OrderId);

  retrieve $Customer from $Order/Module.Order_Customer;
  $ExCust = create Module.ExCustomer (Name = $Customer/Name, Email = $Customer/Email);
  -- Link customer to root...

  -- Export
  $json = export to mapping Module.EMM_Order($Root);
  return $json;
end;
/

Shortcut with View Entities

View Entities (OQL-backed) can retrieve data directly into the export-ready structure, skipping the manual NPE assembly:

create view entity Module.ExOrderView (
  OrderId: integer,
  CustomerName: string,
  CustomerEmail: string
) as select o.OrderId, c.Name, c.Email
   from Module.Order o
   join Module.Order_Customer/Module.Customer c;

This can reduce the microflow to a single retrieve + export step.


Realistic Example: Countries REST API

One worked example — structures, import of a single object and of a list, export in both directions, and the microflow that ties them together — is in reference/rest-api-example.md.

Placing Documents in Folders

Every one of these documents takes a folder clause on create, straight after the qualified name. Missing folders in the path are created:

create json structure Sales.JSON_Order folder 'Private/JSON structures'
  snippet '{"id": 1, "total": 9.99}';

create import mapping Sales.IMM_Order folder 'Private/Import mappings'
  with json structure Sales.JSON_Order
{
  create Sales.Order { OrderId = id, Total = total }
};

On create or modify the clause moves an existing document. Omitting it leaves placement alone — it never returns a document to the module root — so adding a folder to an existing script is safe and removing one is a no-op. describe emits the clause, so a description replays into the same folder.

See organize-project for move and the full folder story.

Common Mistakes

MistakeFix
Reusing import domain model for exportExport needs separate entities mirroring JSON structure
Association direction wrongAlways FROM child TO parent (child owns FK)
Using owner default for 1-1 nested objects in exportUse owner both for 1-1 relationships
Missing array container entity in exportArrays need Container + Item entities
Using key with create handlingkey only valid with find
find without or create / or error / or ignoreSay what happens when the object is not found — the three differ at runtime
find with no member marked key (MDL-MAP02)Mark the identifying member — a search needs something to search on (CE0250)
find over a non-persistent entity (MDL-MAP03)Use create, or make the entity persistent — a search is a database query (CE0251)
Param: parameter with no parameter Module.Entity on the headerDeclare the mapping's input object, or the build reports CE0279
parameter on an EXPORT mappingExport mappings have no input object — their parameter is the root object
Arrays in import with container entityImport arrays map directly to item entity, no container

Signals

GitHub stars
122
Forks
49
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
json-structures-and-mappings
Source
github.com/mendixlabs/mxcli