File format

A model is one JSON file. Here is every field in it.

Overview

A model is stored as *.orm.json: the conceptual schema and its diagram layout in one id-addressed structure. The diagram is a VS Code custom editor over that text document, so the dirty indicator, undo stack, file watching and source control all behave normally — and ORM: Open Model Source (JSON) shows the text at any time.

Elements refer to each other by id, never by position, so hand edits and merges are safe. Missing collections are repaired on load, which means the smallest valid model is {}.

For how this format compares with NORMA .orm, the FBM Exchange MetaModel, the Unified Modelling Schema and Apache Ossie — and why Factum keeps a format of its own — see File formats.

{
  "$schema": "https://www.factum-orm.com/schema/orm-model-2.schema.json",
  "version": 2,
  "name": "HR",
  "objectTypes": [
    { "id": "ot_person", "name": "Person", "kind": "entity",
      "refMode": "nr", "dataType": "integer" },
    { "id": "ot_gender", "name": "GenderCode", "kind": "value",
      "dataType": "string", "dataTypeLength": 1 }
  ],
  "factTypes": [
    {
      "id": "ft_works",
      "roles": [
        { "id": "r1", "objectTypeId": "ot_person" },
        { "id": "r2", "objectTypeId": "ot_company" }
      ],
      "readings": [
        { "id": "rd1", "roleOrder": ["r1", "r2"],
          "text": "{0} works for {1}", "isPrimary": true },
        { "id": "rd2", "roleOrder": ["r2", "r1"], "text": "{0} employs {1}" }
      ]
    }
  ],
  "subtypeRelations": [
    { "id": "st1", "subtypeId": "ot_manager", "supertypeId": "ot_person",
      "isPreferredIdentificationPath": true }
  ],
  "constraints": [
    { "id": "uc1", "kind": "uniqueness", "roles": ["r1"] },
    { "id": "mc1", "kind": "mandatory", "roles": ["r1"] },
    { "id": "vc1", "kind": "value", "objectTypeId": "ot_gender",
      "ranges": [{ "value": "M" }, { "value": "F" }] }
  ],
  "diagram": {
    "shapes": {
      "ot_person": { "x": 120, "y": 200 },
      "ft_works": { "x": 290, "y": 150, "orientation": "horizontal" }
    }
  }
}

Object types

FieldTypeMeaning
idstringUnique within the model
namestringDisplay name; must be unique
kindentity | valueEntity types need a reference scheme; value types are lexical
refModestringReference mode, e.g. nr in Person(.nr)
dataTypestringstring, text, integer, decimal, float, money, boolean, date, time, dateTime, guid, binary, autoCounter
dataTypeLength, dataTypeScalenumberSize for sized types
isIndependentbooleanMay have instances that play no roles — drawn as !
isPersonalbooleanVerbalized with who rather than that
objectifiedFactTypeIdstringSet when this entity type objectifies a fact type
notestringFree text

Fact types, roles and readings

A fact type holds an ordered list of roles and one or more readings. A role names the object type that plays it (objectTypeId, or null while unattached) and may carry a name.

Readings use {0}, {1}, … placeholders indexing into roleOrder — the same convention NORMA uses — so the same fact type can be read in either direction. Mark one reading isPrimary; it is the one shown on the diagram.

A fact type may also be isDerived, with a derivationRule and an optional isStored flag. Derivation rules are stored and verbalized, not evaluated.

A reading may carry a lang — a BCP 47 tag such as en or nl — falling back to the model’s own lang. A multilingual model therefore keeps its languages through a round trip.

Constraints

Every constraint has an id, a kind, an optional name and an optional modality (alethic, the default, or deontic).

kindShape
uniquenessroles: [], optional isPreferredIdentifier
mandatoryroles: [] — one role is simple, several is disjunctive
frequencyroles: [], min, max (null for unbounded)
ringroles: [a, b], types: [] — irreflexive, symmetric, acyclic, …
subset, exclusion, equalityroleSequences: [[…], […]]; for subset, the first is the subset
valueobjectTypeId or roleId, plus ranges: []
cardinalityobjectTypeId or roleId, plus min, max
subtypeSetsupertypeId, subtypeRelationIds: [], isExclusive, isExhaustive

A value range is either a single value, or a min/max pair with optional minInclusive and maxInclusive flags.

Diagram

diagram.shapes maps an element id to its position: x, y, optional w/h, and for fact types an orientation of horizontal or vertical. External constraints may carry their own position; anything without one is placed automatically. Deleting a shape entry simply re-places the element — the model itself is unaffected.

Sample populations

A fact type may carry a population: sample tuples, one value per role in the fact type’s own role order. A value type may carry a flat list of the values it takes.

{
  "id": "ft_works",
  "roles": [ … ],
  "readings": [ { "text": "{0} works for {1}", … } ],
  "population": [
    { "values": ["101", "Acme"] },
    { "values": ["102", "Globex"] }
  ]
}

Populations are what fact-based modelling starts from — the modeller writes example sentences and the model follows. Keeping them in the file makes four things possible:

  • the verbalizer substitutes the real values back into the reading, so a domain expert reads “101 works for Acme” rather than a placeholder;
  • the validator checks the constraints you drew against the examples you gave — a uniqueness, mandatory, frequency or value constraint the data contradicts is reported;
  • the examples become seed data and test fixtures;
  • FBM and NORMA round trips stop discarding the instances they carry.
A population disagreeing with a constraint is an error, not a warning. One of the two is wrong, and either answer is worth having.

Metadata

Every element — the model, object types, fact types, roles, readings, subtype relations and constraints — may carry a meta object. Nothing in it changes what the conceptual schema means; it is there so a model survives a round trip through another fact-based modeling tool, and so generators have something to say beyond an element’s name.

FieldMeaningCounterpart elsewhere
guidStable cross-tool identityFBM GUID, NORMA element ids
uriIRI denoting the elementRDF/OWL, ontology alignment
titleDisplay name when it differs from name
shortDescription, descriptionDocumentationFBM ShortDescription, LongDescription
synonymsAlternative namesFBM Synonyms, Ossie ai_context.synonyms
tagsFree classification, e.g. pii
aiContextinstructions, synonyms, examplesOssie ai_context
sourcetool, version, ref — set by importers

meta.description is not inert: both mappers prefer it to their generated comment, so documentation written once in the model reaches the SQL and the Cypher.

Schema generation hints

A hints object attaches per-target guidance for generation. The rule that keeps hints safe is that a hint never changes the conceptual schema, only how it is rendered — strip every hint from a file and the model still says exactly the same thing. A name given in a hint is a physical name and is emitted verbatim, without the case conversion the mappers otherwise apply.

HintApplies toEffect
relational.schemaNamemodelQualifies every generated table
relational.tableNameobject type, fact typePhysical table name
relational.columnNamevalue type, rolePhysical column name
relational.sqlTypevalue typeSQL type, used instead of the one derived from dataType
relational.mappingfact typeabsorb or separateTable
graph.labelobject type, fact typeNode label or relationship type
graph.labelsobject typeAdditional node labels
graph.propertyNamevalue type, roleName of an absorbed property
graph.mappingvalue typenode or property
{
  "id": "ot_person", "name": "Person", "kind": "entity", "refMode": "nr",
  "meta": {
    "description": "A human being known to the business.",
    "synonyms": ["Party"]
  },
  "hints": {
    "relational": { "tableName": "HR_PERSON" },
    "graph": { "label": "Employee", "labels": ["Party"] }
  }
}

A hint that would make the generated schema lose facts is refused rather than obeyed, and the refusal appears in the mapping notes. Asking for graph.mapping: "property" on a value type played many-to-many is the case that arises in practice: a single-valued property cannot hold many values, so it becomes a node anyway.

Unknown target keys are legal and preserved, so another tool can carry hints.ossie or hints.typedb without a change to this format.

Extensions

Any key beginning with x- is an extension: legal anywhere, ignored by the editor, and written back unchanged. This is the same convention OpenAPI uses, and it is how another tool can round-trip data Factum has no concept for.

The loader is more permissive than the schema on purpose. It preserves every unrecognised top-level key, not only x- ones, so a misspelled key shows up as a warning in the editor rather than disappearing on the next save.

Schema and version

The format is described by a JSON Schema (2020-12) published at schema/orm-model-2.schema.json and shipped with the extension, so VS Code validates and completes a .orm.json file in the plain text editor. Point $schema at it and any other tool, in any language, can validate against the same contract.

version is the format’s major version, currently 2. Version 2 is additive over version 1 — it only adds optional meta, hints and lang keys — so a version 1 file is a valid version 2 file, is upgraded on load, and is written back as version 2 on the next save.