UML Modelling of Elements
Model Forge does not invent a second metamodel for domain modelling. An
Element is — and stays — a JSON Schema 2020-12 document
whose domain identity is a CORE URN in $id. The graphical UML
editor is therefore a projection: every box, edge, attribute and stereotype
you draw is just a different way of looking at the same JSON Schema, and every
edit round-trips losslessly to the stored schema.
This page documents the UML subset the editor supports and shows, feature by feature, the exact JSON Schema / CORE-IR fragment each construct produces. The goal is that you can read a diagram and predict the stored schema, and vice versa, without surprises.
The visual editor described here is a planned authoring UI, not yet
implemented. The UML ↔ JSON-Schema construct mapping
below — class → object, attributes/cardinalities, $ref vs. x-core-ref,
allOf inheritance, enumerations — is the implemented, accurate part and matches
what the API stores. The editor screens are wireframes of the intended UI,
not screenshots of a shipped tool.
JSON Schema is the source of truth, so the UML layer only exposes constructs that have a faithful, round-trippable JSON Schema representation. UML features with no clean schema mapping (e.g. method operations, visibility modifiers, association classes) are deliberately not part of the subset.
Each class on the canvas is its own Element with
its own CORE URN. When a diagram holding several inter-referencing classes is
exported and imported as one JSON Schema document (a root plus $defs), the
import automatically creates a DataStructure — the stored grouping
that references those Elements by URN ("these were authored and imported
together"). The diagram is the authoring surface; the DataStructure is the persisted
grouping it produces. A DataStructure is a stored artifact, not a View.
The editor at a glance
The editor is a three-pane workspace: the toolbox with the five tools on the left, the canvas where you draw classes and relations in the middle, and a property inspector on the right that edits the selected element — including the JSON Schema facets a diagram cannot show. Every box, edge and field maps straight back to the stored schema.
The diagrams on this page are wireframes of the planned editor UI, not screenshots of a shipped tool. They are draw.io-editable SVGs — open them in diagrams.net to view or adjust. They illustrate the interaction model; exact layout and labels will evolve.
Editor toolbox
The whole subset is built from just five toolbox tools. Everything else —
attributes, cardinalities, primitive-type constraints (pattern, format,
ranges) and inline enums — is a property edit inside a class, not a separate
palette item:
| Tool | Creates | Maps to |
|---|---|---|
| Class | an Element (object); its attributes, cardinalities, primitive constraints and inline enums are edited inside the box | features 1, 2, 5b |
| Enumeration | a shared <<enumeration>> type | feature 5a (referenced via $ref) |
| Composition | a composition edge — filled diamond ◆ at the owning end | feature 3a (embeds the target by value via $ref) |
| Relation | an association edge — plain line, role + cardinality on the ends | feature 3b (references the target by URN via x-core-ref) |
| Inheritance | a generalization edge | feature 4 (an allOf branch — single or mixin) |
The two edge tools split exactly along the embed-vs-reference line, and that is
their visual tell: a Composition (◆) embeds an Element by value ($ref),
a Relation (plain line) references one by URN (x-core-ref). See
feature 3.
1. Class → JSON Schema object
A UML class is an Element. It maps to a JSON Schema document of
type: object whose $id is the versioned CORE URN of the artifact. The class
name becomes title; the URN's name segment matches that title.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:core:platform:civitas:element:common:WeatherObservation:6niukwegew:1.0.0",
"title": "WeatherObservation",
"type": "object"
}
An empty class is a valid (if degenerate) Element. Everything below adds keywords to this same object.
2. Properties / attributes
A UML attribute maps to one entry under properties. The attribute type maps
to the property's JSON Schema type; the attribute cardinality (multiplicity)
controls whether the property is required and whether it is wrapped in an
array.
Cardinalities
| UML multiplicity | Meaning | JSON Schema mapping |
|---|---|---|
1..1 | required single value | listed in required, scalar schema |
0..1 | optional single value | not in required, scalar schema |
1..* | required, one or more | in required, type: array with minItems: 1 |
0..* | optional, zero or more | not in required, type: array |
m..n | bounded | type: array with minItems: m, maxItems: n |
The bracketed multiplicities map directly: sensorId [1] → a required scalar,
temperature [0..1] → an optional scalar, and tags [0..*] → an optional array:
{
"$id": "urn:core:platform:civitas:element:common:WeatherObservation:6niukwegew:1.0.0",
"title": "WeatherObservation",
"type": "object",
"properties": {
"sensorId": { "type": "string" },
"temperature": { "type": "number" },
"tags": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["sensorId"]
}
A bounded multiplicity such as readings 1..12 adds the array bounds:
{
"readings": {
"type": "array",
"items": { "type": "number" },
"minItems": 1,
"maxItems": 12
}
}
UML writes multiplicity in square brackets after the attribute, and this page
shows it that way inside the class box (+string tags [0..*]) — Mermaid renders the
bracketed multiplicity as plain attribute text. (Curly braces {…} are reserved in
UML for constraints such as {unique}, not for multiplicity.) On relations the
cardinality and role sit in the edge-end labels instead — "0..* sensors" — and the
edge carries no arrowhead (see the relation section).
Primitive types are described exactly as JSON Schema allows
There is no fixed CORE primitive set. A property is described with the full expressiveness of JSON Schema validation keywords, and the editor surfaces those constraints on the attribute. This keeps the model precise and lets validation, code generation and documentation reuse the same constraints.
A string with a pattern and a format:
{
"properties": {
"stationId": {
"type": "string",
"pattern": "^CS-[0-9]{6}$",
"minLength": 9,
"maxLength": 9
},
"contactEmail": { "type": "string", "format": "email" },
"lastSeenAt": { "type": "string", "format": "date-time" }
}
}
Common format values used in authored Elements include date-time,
date, email and uri. format is an annotation/assertion on a string; it
does not change the underlying type.
A numeric property with a range:
{
"properties": {
"maxPowerKw": {
"type": "number",
"minimum": 0,
"maximum": 350
},
"connectorCount": {
"type": "integer",
"exclusiveMinimum": 0,
"multipleOf": 1
}
}
}
integer and number accept minimum / maximum (inclusive),
exclusiveMinimum / exclusiveMaximum, and multipleOf. boolean is the
simplest case — { "type": "boolean" } — and takes no further constraints.
A UML diagram can only show an attribute's name and type — it cannot depict
pattern, format, minimum / maximum, minLength etc. graphically. In the
editor these JSON-Schema facets are shown and edited in the property inspector of
the selected attribute, so the diagram stays readable while the full constraint set
is one click away.
Selecting an attribute opens its inspector: the basic type and cardinality at the
top, the type-specific constraints (pattern, format, minLength/maxLength,
ranges) below, and a read-only preview of the exact schema fragment the edits
produce — the same JSON that round-trips to storage.
3. Relations: composition vs. association
Two Elements can be linked in two ways, and the difference is exactly the
$ref vs. x-core-ref distinction — now with a visual tell:
- Composition — a filled diamond ◆ at the owning end — embeds the target by
value (
$ref): the property's value is an instance shaped like the target (inlined when an inlined or bundled view is requested). - Association — a plain line — references the target by its CORE URN
(
x-core-ref): only the URN string is stored; the target stays an independent artifact.
In both cases the role at one end names a property in the class at the opposite end, and the cardinality on that end decides single value vs. array.
3a. Composition ($ref) — embedding, filled diamond ◆
A composition embeds the target Element: the value is an instance shaped like the
target; an inlined or bundled view inlines it, while the default read keeps the raw
$ref URN. In stored content it is a plain
JSON Schema $ref to the target's versioned URN — the same mechanism the
Data Modelling page describes
("No CORE-specific extensions are required for structural modelling"). The cardinality on
the owning end decides single ref vs. array of refs.
Here Station embeds Sensor — sensors (0..*) drawn as a composition edge (the
filled diamond sits at Station, the owner), and primarySensor (1) shown as a typed
property in the box. Both are $refs; drawing the single one as a box-row also keeps two
edges from stacking on the same class pair (see
Editor behaviour for the property ⇄ edge duality).
The 0..* sensors composition becomes an optional array of $refs on Station; the
primarySensor property is a single required $ref:
{
"$id": "urn:core:platform:civitas:element:common:Station:hkr6gt1ni1:1.0.0",
"title": "Station",
"type": "object",
"properties": {
"stationId": { "type": "string" },
"primarySensor": {
"$ref": "urn:core:platform:civitas:element:common:Sensor:m8i4hc3h56:1.0.0"
},
"sensors": {
"type": "array",
"items": {
"$ref": "urn:core:platform:civitas:element:common:Sensor:m8i4hc3h56:1.0.0"
}
}
},
"required": ["stationId", "primarySensor"]
}
A 1..* composition is the same array form with minItems: 1 added (and the property
listed in required). Each $ref becomes an edge in the
artifact relations graph and a stored artifact reference.
Composition is naturally one-directional — a back-reference to the owner is better
modelled as an association than as a second embedding.
3b. Association (x-core-ref) — reference by URN, plain line
An association does not embed anything: the property is a plain string holding the
target's CORE URN, marked with x-core-ref. The target
is an independent artifact with its own identity and lifecycle; existence is the registry's
concern (registry-aware validation on import), so JSON Schema only checks the string form
(pattern: "^urn:").
Here a Baum (tree) references the Strasse (street) it stands on — by URN, without
embedding it; and Strasse carries the URNs of the Baums on it, so the association is
bi-directional (each end stores the other's URN, no nesting):
stehtAn is a single URN string on Baum; baeume is an array of URN strings on
Strasse:
{
"$id": "urn:core:platform:civitas:element:common:Baum:vd1c1ziwlu:1.0.0",
"title": "Baum",
"type": "object",
"properties": {
"baumId": { "type": "string" },
"stehtAn": {
"type": "string",
"pattern": "^urn:",
"x-core-ref": { "type": "urn:core:platform:civitas:element:common:Strasse:u8pwgr2zzg:1.0.0" }
}
},
"required": ["baumId", "stehtAn"]
}
A 0..* association is an array of such URN strings (each item carrying the
x-core-ref). Associations also wire the CORE-IR artifacts together — the URN fields
inside a DataSet, Mapping, Pipeline, DataSource or DataSink (a pipeline node's
sourceRef, a DataSet's *Refs).
$ref vs. x-core-ref
These look similar but model different relationships — and now draw differently:
- Composition ·
$ref(filled diamond ◆, standard JSON Schema) — embedding: the value at that position is an instance shaped like the referenced schema, and the target's constraints apply (inlined only in an inlined or bundled view; the default read keeps the raw$refURN). - Association ·
x-core-ref(plain line, CORE extension) — a foreign key: a plain string property whose value names another artifact by its CORE URN; nothing is embedded. The value is always a global URN; existence is the registry's concern. There is nolocal/globalscope.
In short — ◆ $ref: "the value is (shaped like) X"; — x-core-ref: "the value
names artifact X". Both apply to Elements: a WeatherObservation may embed
a GeoPoint ($ref, composition) but reference a Station by URN (x-core-ref,
association). The x-core-ref names the concrete target Element, whose existence
is checked against the registry on import.
$ref maps to composition (the filled diamond): the embedded value is part of the
owning instance and shares its lifetime. UML's aggregation (the hollow diamond ◇) — a
shared part with an independent lifetime — is deliberately not in the subset; a
by-reference link is modelled as a plain x-core-ref association instead. That keeps a
crisp two-way distinction: diamond = embedded, line = referenced.
The $ref may pin a concrete version (…:Sensor:1.0.0) or use the latest
token (…:Sensor:latest) to track the target's current version; see
URN Format.
4. Inheritance → allOf
A subclass maps to allOf: a list combining one or more $refs to the parent
URNs with an inline object that contributes the subclass's own properties. The
subclass is still its own Element with its own $id; it does not copy the
parents' properties, it composes them. allOf takes any number of $ref
branches, so single inheritance and multiple inheritance (mixins) are the same
construct with one or several parents.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:core:platform:civitas:element:common:WeatherObservation:6niukwegew:2.0.0",
"title": "WeatherObservation",
"allOf": [
{ "$ref": "urn:core:platform:civitas:element:common:ObservationBase:7zlmk0utgy:1.0.0" },
{
"type": "object",
"properties": {
"temperature": { "type": "number" }
},
"required": ["temperature"]
}
]
}
An instance must satisfy both branches: every constraint of ObservationBase
and the subclass's own properties. The $ref to the superclass is an ordinary
reference edge in the model graph, so inheritance participates in dependency and
impact analysis like any other relation.
Multiple inheritance (mixins)
Because allOf holds an arbitrary number of $ref branches, a class can compose
several parents at once — each <|-- edge adds one $ref. This is how mixins
work: small reusable Elements (e.g. Timestamped, Geolocated) that a class
pulls in alongside its own properties.
{
"$id": "urn:core:platform:civitas:element:common:WeatherObservation:6niukwegew:3.0.0",
"title": "WeatherObservation",
"allOf": [
{ "$ref": "urn:core:platform:civitas:element:common:Timestamped:n55al1vtd3:1.0.0" },
{ "$ref": "urn:core:platform:civitas:element:common:Geolocated:g9ijk6oh3q:1.0.0" },
{
"type": "object",
"properties": { "temperature": { "type": "number" } },
"required": ["temperature"]
}
]
}
An instance must satisfy every branch — all parents and the own object — so a
WeatherObservation carries observedAt, lat, lon and temperature. Branch
order is irrelevant to validation, and each parent is an independent reference edge
in the relations graph. (allOf has no built-in conflict resolution, so mixins
should contribute disjoint property sets.)
5. Enumerations
An enumeration can be modelled two ways, and the choice maps 1:1 to the schema — the same "referenced vs. inline" duality as relations.
5a. As an explicit enumeration type → referenced external schema
Define the enum as its own <<enumeration>> class (with its own $id) and
reference it from the property. The enum becomes a standalone Element
that any number of others $ref:
The level property is a $ref to the Quality URN — so it is drawn as a composition
edge (filled diamond), like any other $ref. Quality is stored as its own Element
whose schema is the enum:
// AirQuality — references the shared enum
{
"$id": "urn:core:platform:civitas:element:common:AirQuality:wyonmizavr:1.0.0",
"type": "object",
"properties": {
"level": { "$ref": "urn:core:platform:civitas:element:common:Quality:r88o7pjps2:1.0.0" }
},
"required": ["level"]
}
// Quality — its own Element
{
"$id": "urn:core:platform:civitas:element:common:Quality:r88o7pjps2:1.0.0",
"title": "Quality",
"type": "string",
"enum": ["GOOD", "MODERATE", "POOR"]
}
Choose this when the same value set is reused across several Elements — one definition, referenced everywhere, versioned independently.
5b. Inline on the property → inline enum
Write the allowed literals directly on the attribute in the class box, in square
brackets — +[GOOD, MODERATE, POOR] level. It produces the enum keyword
inline on the property, with no separate artifact:
{
"$id": "urn:core:platform:civitas:element:common:AirQuality:wyonmizavr:1.0.0",
"type": "object",
"properties": {
"level": { "type": "string", "enum": ["GOOD", "MODERATE", "POOR"] }
},
"required": ["level"]
}
This is the common case for a one-off value set (e.g. a station status of
["available", "occupied", "offline", "maintenance"]).
The editor lets you flip between the two — promote an inline enum to a shared
<<enumeration>> type, or inline a referenced one — exactly like the
property ⇄ relation toggle (see Editor behaviour).
Editor behaviour
The diagram and the schema are two views of one artifact, so several editor interactions exist purely to switch between views without changing the stored schema. Understanding this duality explains why "deleting" something in the editor asks a clarifying question.
Property ⇄ edge are two views of the same thing
A reference property can be shown either as a row inside the class box or as an edge, and the edge style follows the property kind:
$ref(class-typed) ⇄ composition edge — e.g.+Sensor primarySensorin the box, or a filled-diamond edgeStation *-- "1 primarySensor" Sensor.x-core-ref(URN string) ⇄ association edge — e.g.+→Strasse stehtAnin the box, or a plain-line edgeBaum -- "1 stehtAn" Strasse.
Either way the same underlying JSON Schema property is rendered — a $ref for a
composition, an x-core-ref string for an association (single, or an array for *).
Choosing one view or the other never rewrites the schema; it only changes what is drawn.
primarySensor is rendered as a typed property inside the box; sensors is rendered
as a composition edge. In the stored schema both are simply $ref properties of
Station.
In the following, the same is expressed without relation edge:
(The station back-reference uses the → association notation — an x-core-ref URN,
not a second embedding — per the composition-vs-association rule in section 3a.)
Deleting a relation: hide vs. delete
Because a relation edge is just a view of a property, deleting an edge is ambiguous. The editor therefore prompts:
- Hide only — the edge is removed from the diagram, and the property reappears as a typed row inside the class box. The JSON Schema is unchanged; only the visual representation switched back to the property view.
- Delete completely — the property itself is removed from the schema
(dropped from
propertiesand fromrequired). This is a real model change and removes the corresponding reference edge from the model graph.
"Show as relation" on a complex-typed property
Right-clicking a property whose type is another class offers show as relation. The editor draws the edge from the source class to the referenced class, labelled with the property name as the role. If the referenced class is not yet present on the diagram, it is loaded into the diagram on demand (the target Element is fetched and placed), so the relation can be drawn against a real box rather than a dangling reference.
Cardinalities apply to any property
Cardinality is not exclusive to relations. It can be set on any property — scalar or reference — and maps to the schema exactly as in feature 2:
1..1/0..1→ required/optional single value1..*→ requiredarraywithminItems: 10..*→ optionalarray- bounded →
arraywithminItems/maxItems
So whether a property is drawn inside the box or as an edge, its multiplicity
drives the same required / array / minItems / maxItems decisions.
Summary: UML → JSON Schema / CORE-IR mapping
| UML construct | JSON Schema / CORE-IR mapping |
|---|---|
| Class | type: object document; $id = versioned CORE URN; class name = title |
| Attribute | entry under properties; UML type → JSON Schema type + validation keywords |
| Primitive type | string (pattern, format, minLength/maxLength), integer/number (minimum/maximum/exclusiveMinimum/multipleOf), boolean |
Cardinality 1..1 / 0..1 | required / optional scalar (required list controls it) |
Cardinality 1..* | required array + minItems: 1 |
Cardinality 0..* | optional array |
Cardinality m..n | array + minItems: m, maxItems: n |
| Composition (filled diamond ◆) | property named after the target role; value is $ref to the target URN (embed), or array of $ref for * cardinalities |
Association (plain line, x-core-ref) | property named after the target role; value is a URN string carrying x-core-ref (foreign key), or array of such strings for * |
Inheritance (Base <|-- Derived) | allOf: [ { $ref: <superclass-urn> }, { type: object, properties: … } ] |
| Enumeration | { "type": "string", "enum": [ … ] }, inline on the property or as a referenced Element |
| Property ⇄ edge | two views of the same property ($ref ⇄ composition, x-core-ref ⇄ association); no schema change when switching |