Skip to content

Commit e99a356

Browse files
akoclaude
andcommitted
feat: redesign import/export mapping syntax v2
Assignment-style syntax that reads naturally and removes redundancy: Import: CREATE Module.Entity { Attr = jsonField KEY } Export: Module.Entity { jsonField = Attr } Nested: CREATE Assoc/Entity = jsonKey { ... } Schema: WITH JSON STRUCTURE (replaces FROM/TO) Key changes: - Entity-first: what you're building is prominent, not the JSON field - No type annotations: entity already defines attribute types - Association paths: Assoc/Entity instead of VIA keyword - Handling keywords: CREATE, FIND, FIND OR CREATE before entity - Value transforms: Attr = Module.Microflow(jsonField) (grammar ready) - Commas between elements, consistent with database client mappings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dd810f5 commit e99a356

13 files changed

+9587
-9181
lines changed

.claude/skills/mendix/rest-call-from-json.md

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -65,25 +65,26 @@ CREATE ASSOCIATION Module.MyRootObject_MyNestedObject
6565

6666
```sql
6767
CREATE IMPORT MAPPING Module.IMM_MyMapping
68-
FROM JSON STRUCTURE Module.JSON_MyStructure
68+
WITH JSON STRUCTURE Module.JSON_MyStructure
6969
{
70-
"" AS Module.MyRootObject (Create) {
71-
nestedKey AS Module.MyNestedObject (Create) VIA Module.MyRootObject_MyNestedObject {
72-
name AS name (String),
73-
code AS code (String)
74-
},
75-
stringField AS stringField (String),
76-
intField AS intField (Integer)
70+
CREATE Module.MyRootObject {
71+
stringField = stringField,
72+
intField = intField,
73+
CREATE Module.MyRootObject_MyNestedObject/Module.MyNestedObject = nestedKey {
74+
name = name,
75+
code = code
76+
}
7777
}
7878
};
7979
```
8080

8181
**Syntax rules:**
82-
- Root element uses `""` (empty string) as the JSON key — it maps the top-level object
83-
- Object mappings: `jsonKey AS Module.Entity (Create|Find|FindOrCreate)`
84-
- Value mappings: `jsonKey AS attributeName (String|Integer|Long|Decimal|Boolean|DateTime)`
85-
- `VIA Module.Association` — required when mapping a nested object reachable via an association
86-
- No semicolons between child elements inside `{}`
82+
- Root object: `CREATE Module.Entity { ... }` — always starts with handling keyword
83+
- Value mappings: `AttributeName = jsonFieldName` — entity attribute on the left, JSON field on the right
84+
- Nested objects: `CREATE Association/Entity = jsonKey { ... }` — association path + JSON key
85+
- Object handling: `CREATE` (default), `FIND` (requires KEY), `FIND OR CREATE`
86+
- KEY marker: `Attr = jsonField KEY` — marks the attribute as a matching key
87+
- Value transforms: `Attr = Module.Microflow(jsonField)` — call a microflow to transform the value
8788

8889
**Verify** after creation — check Schema elements are ticked in Studio Pro:
8990
- Open the import mapping in Studio Pro
@@ -166,22 +167,22 @@ CREATE ASSOCIATION Integrations.BibleApiResponse_BibleVerse
166167

167168
-- Step 3: Import Mapping
168169
CREATE IMPORT MAPPING Integrations.IMM_BibleVerse
169-
FROM JSON STRUCTURE Integrations.JSON_BibleVerse
170+
WITH JSON STRUCTURE Integrations.JSON_BibleVerse
170171
{
171-
"" AS Integrations.BibleApiResponse (Create) {
172-
translation AS Integrations.BibleTranslation (Create) VIA Integrations.BibleApiResponse_BibleTranslation {
173-
identifier AS identifier (String),
174-
language AS language (String),
175-
language_code AS language_code (String),
176-
license AS license (String),
177-
name AS name (String)
172+
CREATE Integrations.BibleApiResponse {
173+
CREATE Integrations.BibleApiResponse_BibleTranslation/Integrations.BibleTranslation = translation {
174+
identifier = identifier,
175+
language = language,
176+
language_code = language_code,
177+
license = license,
178+
name = name
178179
},
179-
random_verse AS Integrations.BibleVerse (Create) VIA Integrations.BibleApiResponse_BibleVerse {
180-
book AS book (String),
181-
book_id AS book_id (String),
182-
chapter AS chapter (Integer),
183-
text AS text (String),
184-
verse AS verse (Integer)
180+
CREATE Integrations.BibleApiResponse_BibleVerse/Integrations.BibleVerse = random_verse {
181+
book = book,
182+
book_id = book_id,
183+
chapter = chapter,
184+
text = text,
185+
verse = verse
185186
}
186187
}
187188
};

mdl-examples/doctype-tests/06-rest-client-examples.mdl

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,12 +1207,12 @@ CREATE NON-PERSISTENT ENTITY RestTest.PetResponse (
12071207
/
12081208

12091209
CREATE IMPORT MAPPING RestTest.IMM_Pet
1210-
FROM JSON STRUCTURE RestTest.JSON_Pet
1210+
WITH JSON STRUCTURE RestTest.JSON_Pet
12111211
{
1212-
"" AS RestTest.PetResponse (Create) {
1213-
id AS PetId (Integer, KEY),
1214-
name AS Name (String),
1215-
status AS Status (String)
1212+
CREATE RestTest.PetResponse {
1213+
PetId = id,
1214+
Name = name,
1215+
Status = status
12161216
}
12171217
};
12181218

@@ -1251,22 +1251,55 @@ CREATE ASSOCIATION RestTest.OrderResponse_OrderItem
12511251
/
12521252

12531253
CREATE IMPORT MAPPING RestTest.IMM_Order
1254-
FROM JSON STRUCTURE RestTest.JSON_Order
1254+
WITH JSON STRUCTURE RestTest.JSON_Order
12551255
{
1256-
"" AS RestTest.OrderResponse (Create) {
1257-
orderId AS OrderId (Integer, KEY),
1258-
customer AS RestTest.CustomerInfo (Create) VIA RestTest.OrderResponse_CustomerInfo {
1259-
email AS Email (String),
1260-
name AS Name (String)
1256+
CREATE RestTest.OrderResponse {
1257+
OrderId = orderId KEY,
1258+
CREATE RestTest.OrderResponse_CustomerInfo/RestTest.CustomerInfo = customer {
1259+
Email = email,
1260+
Name = name
12611261
},
1262-
items AS RestTest.OrderItem (Create) VIA RestTest.OrderResponse_OrderItem {
1263-
price AS Price (Decimal),
1264-
quantity AS Quantity (Integer),
1265-
sku AS Sku (String)
1262+
CREATE RestTest.OrderResponse_OrderItem/RestTest.OrderItem = items {
1263+
Sku = sku,
1264+
Quantity = quantity,
1265+
Price = price
12661266
}
12671267
}
12681268
};
12691269

1270+
-- ============================================================================
1271+
-- Level 13.3: Find Or Create Import Mapping
1272+
-- ============================================================================
1273+
--
1274+
-- Uses FIND OR CREATE to upsert: find existing by KEY, create if not found.
1275+
1276+
CREATE IMPORT MAPPING RestTest.IMM_UpsertPet
1277+
WITH JSON STRUCTURE RestTest.JSON_Pet
1278+
{
1279+
FIND OR CREATE RestTest.PetResponse {
1280+
PetId = id KEY,
1281+
Name = name,
1282+
Status = status
1283+
}
1284+
};
1285+
1286+
-- ============================================================================
1287+
-- Level 13.4: Import Mapping with Value Transform
1288+
-- ============================================================================
1289+
--
1290+
-- Demonstrates calling a microflow to transform a JSON value during import.
1291+
-- The microflow receives the raw JSON field value and returns the converted type.
1292+
1293+
-- CREATE IMPORT MAPPING RestTest.IMM_TransformPet
1294+
-- WITH JSON STRUCTURE RestTest.JSON_Pet
1295+
-- {
1296+
-- CREATE RestTest.PetResponse {
1297+
-- PetId = id,
1298+
-- Name = RestTest.TrimAndUpperCase(name),
1299+
-- Status = status
1300+
-- }
1301+
-- };
1302+
12701303
-- ############################################################################
12711304
-- PART 14: EXPORT MAPPINGS
12721305
-- ############################################################################
@@ -1280,12 +1313,12 @@ CREATE IMPORT MAPPING RestTest.IMM_Order
12801313
-- Maps an entity back to JSON for outgoing REST calls.
12811314

12821315
CREATE EXPORT MAPPING RestTest.EMM_Pet
1283-
TO JSON STRUCTURE RestTest.JSON_Pet
1316+
WITH JSON STRUCTURE RestTest.JSON_Pet
12841317
{
1285-
RestTest.PetResponse AS root {
1286-
PetId AS id (Integer),
1287-
Name AS name (String),
1288-
Status AS status (String)
1318+
RestTest.PetResponse {
1319+
id = PetId,
1320+
name = Name,
1321+
status = Status
12891322
}
12901323
};
12911324

@@ -1296,19 +1329,19 @@ CREATE EXPORT MAPPING RestTest.EMM_Pet
12961329
-- Demonstrates configurable null value handling (LeaveOutElement vs SendAsNil).
12971330

12981331
CREATE EXPORT MAPPING RestTest.EMM_Order
1299-
TO JSON STRUCTURE RestTest.JSON_Order
1332+
WITH JSON STRUCTURE RestTest.JSON_Order
13001333
NULL VALUES LeaveOutElement
13011334
{
1302-
RestTest.OrderResponse AS root {
1303-
OrderId AS orderId (Integer),
1304-
RestTest.CustomerInfo VIA RestTest.OrderResponse_CustomerInfo AS customer {
1305-
Email AS email (String),
1306-
Name AS name (String)
1335+
RestTest.OrderResponse {
1336+
orderId = OrderId,
1337+
RestTest.OrderResponse_CustomerInfo/RestTest.CustomerInfo AS customer {
1338+
email = Email,
1339+
name = Name
13071340
},
1308-
RestTest.OrderItem VIA RestTest.OrderResponse_OrderItem AS items {
1309-
Price AS price (Decimal),
1310-
Quantity AS quantity (Integer),
1311-
Sku AS sku (String)
1341+
RestTest.OrderResponse_OrderItem/RestTest.OrderItem AS items {
1342+
price = Price,
1343+
quantity = Quantity,
1344+
sku = Sku
13121345
}
13131346
}
13141347
};

mdl/ast/ast_import_export_mapping.go

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ package ast
99
// CreateImportMappingStmt represents:
1010
//
1111
// CREATE IMPORT MAPPING Module.Name
12-
// [FROM JSON STRUCTURE Module.JsonStructure | FROM XML SCHEMA Module.Schema]
13-
// { root AS Module.Entity (Create) { ... } }
12+
// WITH JSON STRUCTURE Module.JsonStructure
13+
// {
14+
// CREATE Module.Entity {
15+
// PetId = id KEY,
16+
// Name = name
17+
// }
18+
// };
1419
type CreateImportMappingStmt struct {
1520
Name QualifiedName
1621
SchemaKind string // "JSON_STRUCTURE" or "XML_SCHEMA" or ""
@@ -28,19 +33,23 @@ type DropImportMappingStmt struct {
2833
func (s *DropImportMappingStmt) isStatement() {}
2934

3035
// ImportMappingElementDef represents one element in the mapping tree.
31-
// It may be an object mapping (AS entity) or a value mapping (AS attribute).
3236
type ImportMappingElementDef struct {
33-
// JSON field name (or "root" for the root element)
34-
JsonName string
35-
// Object mapping fields (set when mapping to an entity)
37+
// Object mapping fields
3638
Entity string // qualified entity name (e.g. "Module.Customer")
37-
ObjectHandling string // "Create", "Find", "FindOrCreate", "Custom"
38-
Association string // qualified association name for via clause
39+
ObjectHandling string // "Create", "Find", "FindOrCreate"
40+
Association string // qualified association name (from Assoc/Entity path)
3941
Children []*ImportMappingElementDef
40-
// Value mapping fields (set when mapping to an attribute)
41-
Attribute string // attribute name (unqualified, e.g. "Name")
42-
DataType string // "String", "Integer", "Boolean", "Decimal", "DateTime"
42+
43+
// Value mapping fields
44+
Attribute string // entity attribute name (LHS of =)
4345
IsKey bool
46+
47+
// Shared
48+
JsonName string // JSON field name (RHS of = for both values and objects)
49+
50+
// Value transform via microflow
51+
Converter string // microflow qualified name (e.g. "Module.ConvertStringToDate")
52+
ConverterParam string // json field passed to converter
4453
}
4554

4655
// ============================================================================
@@ -50,9 +59,13 @@ type ImportMappingElementDef struct {
5059
// CreateExportMappingStmt represents:
5160
//
5261
// CREATE EXPORT MAPPING Module.Name
53-
// [TO JSON STRUCTURE Module.JsonStructure | TO XML SCHEMA Module.Schema]
54-
// [NULL VALUES LeaveOutElement | SendAsNil]
55-
// { Module.Entity AS root { ... } }
62+
// WITH JSON STRUCTURE Module.JsonStructure
63+
// {
64+
// Module.Entity {
65+
// jsonField = Attr,
66+
// Module.Assoc/Module.Child AS jsonKey { ... }
67+
// }
68+
// };
5669
type CreateExportMappingStmt struct {
5770
Name QualifiedName
5871
SchemaKind string // "JSON_STRUCTURE" or "XML_SCHEMA" or ""
@@ -71,15 +84,15 @@ type DropExportMappingStmt struct {
7184
func (s *DropExportMappingStmt) isStatement() {}
7285

7386
// ExportMappingElementDef represents one element in an export mapping tree.
74-
// It may be an object mapping (entity AS JSON key) or a value mapping (attribute AS JSON key).
7587
type ExportMappingElementDef struct {
76-
// JSON field name (the RHS of AS)
77-
JsonName string
78-
// Object mapping fields (set when mapping from an entity)
88+
// Object mapping fields
7989
Entity string // qualified entity name (e.g. "Module.Customer")
80-
Association string // qualified association name for VIA clause
90+
Association string // qualified association name (from Assoc/Entity path)
8191
Children []*ExportMappingElementDef
82-
// Value mapping fields (set when mapping from an attribute)
83-
Attribute string // attribute name (unqualified, e.g. "Name")
84-
DataType string // "String", "Integer", "Boolean", "Decimal", "DateTime"
92+
93+
// Value mapping fields
94+
Attribute string // entity attribute name (RHS of =)
95+
96+
// Shared
97+
JsonName string // JSON field name (LHS of = for values, RHS of AS for objects)
8598
}

mdl/executor/cmd_export_mappings.go

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,9 @@ func (e *Executor) describeExportMapping(name ast.QualifiedName) error {
119119
fmt.Fprintf(e.output, "CREATE EXPORT MAPPING %s.%s\n", moduleName, em.Name)
120120

121121
if em.JsonStructure != "" {
122-
fmt.Fprintf(e.output, " TO JSON STRUCTURE %s\n", em.JsonStructure)
122+
fmt.Fprintf(e.output, " WITH JSON STRUCTURE %s\n", em.JsonStructure)
123123
} else if em.XmlSchema != "" {
124-
fmt.Fprintf(e.output, " TO XML SCHEMA %s\n", em.XmlSchema)
124+
fmt.Fprintf(e.output, " WITH XML SCHEMA %s\n", em.XmlSchema)
125125
}
126126

127127
if em.NullValueOption != "" && em.NullValueOption != "LeaveOutElement" {
@@ -131,47 +131,46 @@ func (e *Executor) describeExportMapping(name ast.QualifiedName) error {
131131
if len(em.Elements) > 0 {
132132
fmt.Fprintln(e.output, "{")
133133
for _, elem := range em.Elements {
134-
printExportMappingElement(e, elem, 1)
134+
printExportMappingElement(e, elem, 1, true)
135135
fmt.Fprintln(e.output)
136136
}
137137
fmt.Fprintln(e.output, "};")
138138
}
139139
return nil
140140
}
141141

142-
func printExportMappingElement(e *Executor, elem *model.ExportMappingElement, depth int) {
142+
func printExportMappingElement(e *Executor, elem *model.ExportMappingElement, depth int, isRoot bool) {
143143
indent := strings.Repeat(" ", depth)
144144
if elem.Kind == "Object" {
145-
via := ""
146-
if elem.Association != "" {
147-
via = " VIA " + elem.Association
145+
if isRoot {
146+
// Root: Module.Entity {
147+
fmt.Fprintf(e.output, "%s%s {\n", indent, elem.Entity)
148+
} else {
149+
// Nested: Assoc/Entity AS jsonKey {
150+
fmt.Fprintf(e.output, "%s%s/%s AS %s", indent, elem.Association, elem.Entity, elem.ExposedName)
151+
if len(elem.Children) > 0 {
152+
fmt.Fprintln(e.output, " {")
153+
}
148154
}
149155
if len(elem.Children) > 0 {
150-
fmt.Fprintf(e.output, "%s%s%s AS %s {\n", indent, elem.Entity, via, elem.ExposedName)
151156
for i, child := range elem.Children {
152-
printExportMappingElement(e, child, depth+1)
157+
printExportMappingElement(e, child, depth+1, false)
153158
if i < len(elem.Children)-1 {
154159
fmt.Fprintln(e.output, ",")
155160
} else {
156161
fmt.Fprintln(e.output)
157162
}
158163
}
159164
fmt.Fprintf(e.output, "%s}", indent)
160-
} else {
161-
fmt.Fprintf(e.output, "%s%s%s AS %s", indent, elem.Entity, via, elem.ExposedName)
162165
}
163166
} else {
164-
// Value mapping
167+
// Value mapping: jsonField = Attr
165168
attrName := elem.Attribute
166169
// Strip module prefix if present (Module.Entity.Attr → Attr)
167170
if parts := strings.Split(attrName, "."); len(parts) == 3 {
168171
attrName = parts[2]
169172
}
170-
dt := elem.DataType
171-
if dt == "" {
172-
dt = "String"
173-
}
174-
fmt.Fprintf(e.output, "%s%s AS %s (%s)", indent, attrName, elem.ExposedName, dt)
173+
fmt.Fprintf(e.output, "%s%s = %s", indent, elem.ExposedName, attrName)
175174
}
176175
}
177176

@@ -295,10 +294,7 @@ func buildExportMappingElementModel(moduleName string, def *ast.ExportMappingEle
295294
// Value mapping — qualify attribute name as Module.Entity.Attribute
296295
elem.Kind = "Value"
297296
elem.TypeName = "ExportMappings$ValueMappingElement"
298-
elem.DataType = def.DataType
299-
if elem.DataType == "" {
300-
elem.DataType = "String"
301-
}
297+
elem.DataType = "String" // default; entity already defines the real type
302298
attr := def.Attribute
303299
if parentEntity != "" && !strings.Contains(attr, ".") {
304300
attr = parentEntity + "." + attr

0 commit comments

Comments
 (0)