Working with Multiple Files
Real projects split configuration across files: the platform team ships a baseline, your project adds its parts, a generator contributes derived values.
This chapter shows what that looks like from the configuration author’s side: contributing to shared entities, merge conflicts, and priority overrides with $fileType.
The Definition Decides What Is Allowed
Whether a configuration may span several files is not your choice; the definition declares it in its configurationFileMode:
| Mode | What you may do |
|---|---|
|
Exactly one configuration file for this definition. A second file is an error. |
|
Several files, but each entity lives completely in one file. The top level multiplicities count across the files of a CfgSpace. |
|
Several files may contribute to the same entity; equal contributions conflict. |
|
Like mergeable, plus a file level priority ( |
If you split where the definition says SingleFile, or set a $fileType where priorities are not enabled, validation tells you.
Merging always stays within one CfgSpace; files in different CfgSpaces never see each other.
Splitting Without Merging: MultipleFiles
Nothing merges in the plain MultipleFiles mode.
Every entity lives completely in one file, and an identity may not repeat across the files.
The files still share the document root, and that is where the multiplicity rules differ by level.
- Top level
-
The entities directly under the document root are counted across all files of the definition in one CfgSpace. A
1..1entity is written in exactly one of the files, a3..5list may hold two entries in one file and three in another, and a1..*list needs at least one entry in some file. A file on its own may therefore miss a required top level entity, or hold more entries of a list than its maximum allows. Files in another CfgSpace do not count. - Below the top level
-
Everything nested in a top level entity belongs to that entity and so to the one file declaring it. Its multiplicities are checked within that file, as in a single file configuration.
A top level violation names the relation in every file writing it, or, when no file writes it, the root of every file, as 'parts' has too few elements across the files of the CfgSpace.
or 'req' is required, but is missing in all files of the CfgSpace.
A top level entity may also bound each file on its own with fileMultiplicity, next to the multiplicity counted across the files, as fileMultiplicity: "1..1" for exactly one entity per file.
The export holds one object per definition, so cfu cfg export-merged puts the top level lists of all your files together there.
With --with-origin the root of that object records every file that contributed to it.
Contributing to a Shared Entity
In the mergeable modes, entities with the same identity (name or id) and the same type are merged.
The CanStack example splits one CAN channel between two files.
The platform file owns the channel and its base timing:
{
$definition: "CanStack@1.0",
channels: [
{
name: "Body",
baudRate: 500,
messages: [
{
name: "EcuStatus",
canId: "0x1A0",
cycleTime: 100
}
]
}
]
}
The project file adds its own message to the same channel, matched by name: "Body":
{
$definition: "CanStack@1.0",
channels: [
{
name: "Body",
// baudRate is contributed by the platform file.
// Setting a different value here would be a merge conflict.
busOffRecovery: false,
messages: [
{
name: "DoorState",
canId: "0x2B4",
dlc: 2,
cycleTime: 50
}
]
}
]
}
The merged model contains one Body channel with baudRate, busOffRecovery, and both messages.
To see the merged result as one file:
cfu cfg export-merged -o merged.json .
Add --with-origin and every value carries an _origin annotation with the file and the position it came from, which is the fastest way to answer "who set this?".
The format is specified in Origin Annotations.
Two things follow from merging by identity:
-
Spell the identity identically in every contributing file. A channel
"Body"in one file and"body"in another are two different channels, and the difference typically surfaces later as a multiplicity or reference error. -
Required elements are checked on the merged entity, not per file. Your file may legitimately omit
baudRatebecause the platform file provides it; the corresponding diagnostics say "across merged files" when the merged result is still incomplete.
Fragments: A File for One Nested Entity
Contributing to a nested entity means repeating the structure above it in every file, which grows tedious the deeper the entity sits.
A fragment skips that: it names the anchor entity with a top-level $path / $type pair and then holds only that entity’s own properties.
A third file adds another message to the same Body channel, without spelling out the channels list:
{
$definition: "CanStack@1.0",
$path: "/CanStack/Body",
$type: "CanStack::Channel",
messages: [
{
name: "LightRequest",
canId: "0x3C8",
dlc: 1,
cycleTime: 20
}
]
}
The merged model is the same as if those messages had been written inside the Body channel of one of the other files.
-
$pathis the absolute entity path, the same form an absolute reference uses. Its first segment is the document root’s name, which is the definition name unless a file sets a top-levelname. -
$typeis the type path of the anchor. It may name the anchor’s own type or one it inherits from. The properties the file may set are the ones that type declares, so a supertype anchor accepts only the supertype’s properties, not those of the concrete subtype. -
Both keys are required together, and both are rejected outside the two mergeable modes.
-
Some other source has to declare the anchor: an ordinary file, a mixin, or the content of another fragment. A fragment extends an entity, it never creates one, and a
$paththat nothing declares is always an error. -
nameandidon a fragment root are an error; the last$pathsegment is the entity’s identity. -
Everything else stays as it is:
$fileTypedecides priority, several fragments may share one anchor, and the merge, conflict and multiplicity rules are the ones above.
Reusing a Block of Content
A fragment holds the content of one entity, selected by its path. The same idea with a type instead of a path is a mixin: content written once and applied to every element of a type, or to the ones naming it. The merge, priority and conflict rules below apply to both.
Merge Conflicts
Without priorities all files are equal. If two files set the same parameter of the same entity to different values, validation reports a conflict naming both locations. The fix is organizational, not syntactic: each parameter needs one owning file. Setting the same value twice is tolerated but pointless.
A list-valued parameter or a struct list never concatenates across files, its entries overlay by index. A file supplying three entries and one supplying a single entry therefore contest index 0 alone, and the indexes 1 and 2 come from the first file.
A struct or primitive union holds one member, so two files writing different members conflict like two values, and under priorities the higher member replaces the whole value. When one of the members is an entity, the files conflict at any priority. The losing entity would still exist for references while no property holds it.
Overriding with Priorities
Under MultipleFilesEntitiesMergeableWithPriority each file declares its priority with $fileType, and on a conflict the higher priority wins silently.
A parameter or struct list still overlays by index, the higher file wins only the indexes it writes and cannot shorten or replace a longer list from a lower file.
Entity lists are not affected, their entries merge by identity.
The Nm example ships platform timing at the default priority:
{
$definition: "Nm@1.0",
$fileType: "UserConfig",
nmNodes: [
{
name: "Gateway",
timeoutTime: 2000,
repeatMessageTime: 1600
},
{
name: "Diagnostic",
timeoutTime: 5000
}
]
}
The project overrides one value from a higher priority file:
{
$definition: "Nm@1.0",
$fileType: "UserConfigPriorityHigh",
nmNodes: [
{
name: "Gateway",
timeoutTime: 1500
}
]
}
The merged Gateway node has timeoutTime: 1500 (project wins) and repeatMessageTime: 1600 (only the platform file sets it).
Only equal priorities conflict.
The file types you write in a .cfg.json5, from lowest to highest priority:
-
GeneratedBelowUserConfig, generated content you may override -
UserConfigPriorityLow -
UserConfig, the default when$fileTypeis omitted -
UserConfigPriorityHigh -
GeneratedAboveUserConfig, generated content that overrides you -
UserOverride, the last word short of preconfiguration
Below and above this range live the DefaultValues* and PreConfiguration* types.
Those are reserved for .default.def.json5 and .preconfig.def.json5 files that are part of the definition; using them in a .cfg.json5 is an error.
You will meet them as the reason a value exists although no configuration file sets it, or why your override loses against a preconfigured value.
The full ladder is in the reference.
Keep the priority spread minimal.
A baseline at UserConfig and deliberate overrides at UserConfigPriorityHigh are easy to reason about; five layers of priorities are not.
cfu cfg export-merged --with-origin shows which file won each value: the entries of one element are ordered from the weakest source to the strongest, so the last one names the
file that set the value.
Removing an Entity
Priorities override a value a lower file contributed, they never make an entity absent.
An entity carrying $remove: true is removed from the merged model.
The platform file above ships a Diagnostic node next to Gateway.
An override file drops it:
{
$definition: "Nm@1.0",
$fileType: "UserOverride",
nmNodes: [
{
name: "Diagnostic",
$remove: true
}
]
}
A removal overrides only the files below it in the priority ladder.
The object carries the identity of its target and nothing else, its name, its id, or the property a nameSource / idSource names.
A reference resolving a removed entity is an error.
A fragment anchored at or below it is removed with it, which is how a higher file drops a fragment.
A $remove addressing an entity no file declares is a warning.
Only a user-editable file may remove: DefaultValuesProject, the UserConfig types, PreConfigurationProject and UserOverride.
A $remove in a generated or a shipped default or preconfiguration file is an error and removes nothing.
Warnings and errors inside the removed content of a lower file are not reported, that content never reaches the merged model.
The editor fades each removed entity instead.
The key requires MultipleFilesEntitiesMergeableWithPriority.
Default and Preconfiguration Files
.default.def.json(5) and .preconfig.def.json(5) files are configuration content authored by the definition developer, and they are part of the definition.
You cannot change them: rename and other edits through the tooling are refused, and their values enter your configuration only through merging.
Default/preconfig files never activate a definition on their own.
Per CfgSpace and per definition, they are applied to the merged result only when at least one of your .cfg.json(5) files declares that definition.
Without such a file the default/preconfig content is ignored.
It does not appear in cfu cfg export-merged, and no required-entity checks run for that definition.
Activation is per definition, not per entity, and a minimal file is enough to opt-in:
{
$definition: "Nm@1.0",
}
This activates all default and preconfiguration content of Nm as-is.
The merged output then equals the default/preconfig values.
A reference in one of your files must not target an entity that only exists in default/preconfig files of a definition that is not activated in your CfgSpace. The target would be missing from the merged output, so this is an error; add a configuration file for that definition to activate it. References written inside default/preconfig files may point at default/preconfig content of other definitions, because both sides only become reachable together.
-
Multi-File Configurations and Merging in the developer manual covers the same modes from the definition side, including workspace wide policies.