Type Reference
This chapter describes every element type a definition can declare, with its options and an example.
The examples come from the SensorEcu running example, a sensor acquisition function of a body ECU; the complete files are in the examples folder of this manual and are
validated by the test suite.
Every element consists of exactly one type keyword (entity, struct, union, typeRef, ref, or a scalar such as u8) plus the common element keys: title, description,
$comment, multiplicity, scope, typeName, inherits, embeddedTypeHint, allowPropertyNameDeviation, and _-prefixed annotations.
There is no null type: an absent value is an omitted optional element, never a null in the configuration (see the multiplicity section of the definition
system).
Boolean
bool with an optional default:
sensorSupplyVoltage: {
description: "Nominal supply voltage of the sensor rail.",
f64: {
min: 4.5,
max: 5.5,
unit: "V",
default: 5.0
}
},
selfTest: {
description: "Run the sensor self test during ECU startup.",
bool: {
default: true
}
},
Integers
Eight integer types select the storage width and signedness the embedded software expects: u8, i8, u16, i16, u32, i32, u64, i64.
| Option | Meaning |
|---|---|
|
Value range, defaults to the full type range |
|
Value applied when the configuration omits the parameter |
|
Physical unit of the stored value, see Units |
|
UI hint, |
adcBaseAddress: {
title: "ADC register base address",
multiplicity: "1..1",
u32: {
min: 0x40000000,
max: 0x4FFFFFFF,
display: "hex"
}
},
sampleTime: {
description: "Time between two acquisition cycles.",
u16: {
min: 1,
max: "10 s",
unit: "ms",
default: 100
}
},
On the configuration side, integer values can be written as plain numbers (sampleTime: 50), as strings in decimal or hex notation, both with _ separators for readability, a decimal one also with an exponent when the value is whole ("1e3"), or with a unit:
adcBaseAddress: "0x4000_1000",
display: "hex" only affects how tooling presents the value; it does not change what the configuration author may write.
It cannot be combined with a unit, as a hex value takes none.
Float
f64 is the floating point type, with the same min, max, default, unit, and display options as the integers (see the sensorSupplyVoltage example above).
There is no f32 type; if the embedded software uses a narrower float, document it with embeddedTypeHint.
A float value may also be written as a string ("1.5e3").
Units
unit names the unit the value is stored in.
A built-in unit takes prefixes, so a configuration may write "2 MHz" for a parameter stored in kHz, and Confµgami normalizes it to 2000:
| Unit | Accepted prefixes |
|---|---|
|
|
|
|
|
SI ( |
-
unit: "kHz"stores kilohertz. ForBytesandbitthe base prefix picks the family ("kB"is SI,"KiB"and a bare"Bytes"binary). -
The object form
{ symbol, basePrefix, prefixFamily }gives a custom unit the SI prefixesptoP, which itsbasePrefixopts into:unit: { symbol: "V", basePrefix: "m" }stores millivolts,basePrefix: ""the bare volt. Without abasePrefixa custom symbol is a literal unit, likeunit: "V". Asymbolspelling a prefixed unit of a built-in, such as"ms", is an error, the short formunit: "ms"declares it.prefixFamily(SIorBinary) picks the family ofBytesandbit, and is required with a non-emptybasePrefixonBytes. -
A short form that reads like a built-in unit but is none, a built-in symbol behind a prefix in another case (
"KHz") or a prefix it does not take ("ks"), is a literal unit with a warning. The object form{ symbol: "ks" }declares such a literal unit on purpose. -
min,maxanddefaultaccept the same strings as a configuration value, unit strings such asmax: "10 s"above as well as hex such asmax: "0xFFFF".
An integer must be convertible to a whole number in the stored unit, so "1.5 ms" is an error for a parameter stored in ms.
Tooling shows each value with the best-fitting prefix.
String
| Option | Meaning |
|---|---|
|
Length bounds, in characters (Unicode code points) |
|
Regular expression the value must match |
|
Closed set of allowed values, nothing else is accepted |
|
Suggested values offered in completion, other strings remain valid |
|
Default value |
literalsExhaustive and literalsNonExhaustive are mutually exclusive, and every literal must itself satisfy pattern if one is set.
shortName: {
title: "ECU short name",
description: "Short name of the ECU as visible in diagnostics.",
multiplicity: "1..1",
string: {
minLength: 1,
maxLength: 32,
pattern: "^[A-Za-z][A-Za-z0-9_]*$"
}
},
mountingPosition: {
description: "Mounting position of the ECU in the vehicle.",
string: {
literalsExhaustive: [
"front-left",
"front-right",
"rear-left",
"rear-right"
],
default: "front-left"
}
},
|
A pattern is a JavaScript (ECMA-262) regular expression, because that is what JSON Schema’s
Patterns are also unanchored: |
Choose between enum and string literals deliberately: enum literals must be identifiers and become real enum types in generated code; string literals may be arbitrary strings
("front-left") and stay strings in generated code.
Enum
An enum declares a closed set of identifier literals, here from the TurnIndicator example:
comfortMode: {
description: "Behavior when the driver tips the indicator stalk.",
enum: {
literals: [
"OFF",
"THREE_FLASHES",
"FIVE_FLASHES"
],
default: "THREE_FLASHES"
}
}
Literals are SCREAMING_SNAKE_CASE identifiers (they become enum constants in generated Rust and Python code) and must be unique, ignoring case.
A literal off the convention is an error, allowLiteralNameDeviation: true on the enum allows it for literals spelled by an external format.
default must be one of the literals.
Linker Symbol
linkerSymbol types a parameter as the name of a symbol resolved by the embedded linker, for example a callback function:
errorHook: {
description: "C function called when a sensor failure is detected, e.g. to report a DTC.",
embeddedTypeHint: "void (*errorHook)(uint8 sensorId)",
linkerSymbol: {
language: "C"
}
},
language is required and currently supports "C"; values are checked against the C identifier rules.
minLength, maxLength, and default are available like on strings.
The embeddedTypeHint shown here is a documentation hint rendered in hover and inlay hints; it carries no semantics but tells the integrator the expected C signature.
It applies to value types only and is rejected on an entity and a struct.
File Path
filePath types a parameter as the path to a file or folder.
Validation resolves the path and checks it on disk:
linkerScript: {
title: "Linker script",
description: "The linker script, relative to the configuration file naming it.",
multiplicity: "1..1",
filePath: {
pathKinds: ["File"],
globPattern: "*.ld",
baseResolutionFolder: "SourceFolder",
},
},
outputDir: {
title: "Output folder",
description: "Where the build writes, created by the first build.",
filePath: {
pathKinds: ["Folder", "NonExisting"],
baseResolutionFolder: "CfgSpace",
support: ["AbsPath", "RelPath", "VariableResolution"],
},
},
| Option | Meaning |
|---|---|
|
What the path may point to: |
|
The folder a relative path starts from: |
|
The forms a value may use, at least one of |
|
A pattern the value must match, see below. |
|
What the merged model holds, default |
|
Default value, checked for its form and against |
globPattern reads like a .gitignore line.
*, ?, {a,b}, [0-9] and [!0-9] match within one path segment.
-
Without a
/, the pattern matches the file name, and an existing folder is not checked:.ld,.{png,gif,jpg}. -
With a
/, it matches the path relative to the base folder, files and folders alike:src//*.{c,h}.stands for any number of folders and only as a whole segment. A path outside the base folder never matches. -
Without
RelPathinsupport, every value is absolute, so a pattern with a/matches the absolute path, a leading/standing for the root:/etc/logs/*.log.
The merged model holds the resolved path relative to the CfgSpace root, starting with .. when the target lies outside it.
The merge, constraints and the export all see this form, so two files naming the same target through different relative paths agree.
resolveFilePathOnExport: false keeps the value as written instead, validation still checks it on disk.
A default has no file to resolve against, so it always stays as written.
A constraint therefore matches it with a resolved value only when it is written relative to the CfgSpace root.
Entity
An entity aggregates children and has identity (name/id) in the configuration.
Entities are the only elements that can be referenced by ref and merged across files:
sensors: {
title: "Sensors",
description: "The sensors acquired by this ECU.",
typeName: "Sensor",
multiplicity: "1..*",
inherits: "SensorEcu::SensorBase",
entity: {
input: {
description: "The electrical input of the sensor.",
multiplicity: "1..1",
union: {
typeRefs: [
"SensorEcu::AnalogInput",
"SensorEcu::DigitalInput"
]
}
},
inputFilter: {
description: "Filter applied before scaling.",
typeRef: "SensorEcu::Filter"
},
outputFilter: {
description: "Filter applied after scaling.",
typeRef: "SensorEcu::Filter"
},
calibration: {
description: "Two point calibration of the sensor.",
struct: {
offset: {
multiplicity: "1..1",
f64: {}
},
factor: {
multiplicity: "1..1",
f64: {}
}
}
}
}
},
This also demonstrates inherits: the sensor entity extends the reusable SensorEcu::SensorBase type, see Reusable types.
A configured list of sensors then looks like this:
{
name: "OilTemp",
input: {
$type: "SensorEcu::AnalogInput",
channel: 3,
voltageRange: "RANGE_10V"
},
inputFilter: {
cutoff: "10 Hz",
order: 2
},
calibration: {
offset: -40.0,
factor: 0.125
}
},
Struct
A struct groups parameters without identity.
Use it when the group is pure data and never needs to be referenced or merged on its own:
calibration: {
description: "Two point calibration of the sensor.",
struct: {
offset: {
multiplicity: "1..1",
f64: {}
},
factor: {
multiplicity: "1..1",
f64: {}
}
}
}
Structs cannot contain entities, cannot inherit, and have no name/id in the configuration.
Choosing Between Entity and Struct
Both aggregate children; the difference is identity. An entity is a named object boundary in the domain model, a struct is local composition under its parent.
Choose entity when:
-
Other elements need to point at the object (
refTargetTypesonly accepts entity types). -
The object is a thing in the domain with its own identity, a sensor, a CAN channel, an NM node, rather than a bundle of settings.
-
Contributions from several configuration files should merge into one object (cross-file merging matches entities by identity).
Choose struct when:
-
The fields are just a grouped value under one parent, like a calibration or a filter setting.
-
Nothing ever needs to reference the group on its own.
-
You want to keep the model compact and local.
The choice is visible in the configuration and the generated schemas: an entity accepts the common keys name, id, $description, $comment, and (as a union member) the
$type discriminator.
name identifies the instance among its siblings and id globally; an instance takes one or the other, never both, and the id doubles as the name.
A struct accepts none of these, it is anonymous by construction.
Reuse is not the differentiator: both entity and struct types can be declared under types and shared via typeRef and scope: "public", like the SensorEcu::Filter struct in
the running example.
When in doubt, start with a struct; turning it into an entity later is a smaller change than taking identity away from something other files already reference.
Union
A union lets the configuration author choose between several types:
input: {
description: "The electrical input of the sensor.",
multiplicity: "1..1",
union: {
typeRefs: [
"SensorEcu::AnalogInput",
"SensorEcu::DigitalInput"
]
}
},
The members are named type paths (entities or structs, at least two).
By default unions are discriminated: the configuration states the chosen type with $type:
input: {
$type: "SensorEcu::AnalogInput",
channel: 3,
voltageRange: "RANGE_10V"
},
When to Use a Union
Use a union when one property must accept one of several structured types: mutually exclusive variants with different payload fields, like the analog and digital input above.
If the variants carry no fields of their own and you only need a mode switch, use an enum instead; it is simpler to configure and generates a plain enum type.
How to Model It
-
Declare each member as a reusable type under
typesand list the members with fully qualified type paths intypeRefs. -
Use entity or struct members. A discriminated union rejects scalar (parameter) and
refmembers, because they cannot carry a$typediscriminator. -
Do not make a list of a union that mixes entity and struct members. Entities merge across files by path and structs by index, so such a list has no merge rule and is an error.
-
Leave
allowTypeAmbiguityat its defaultfalseunless you intentionally need overlapping shapes.
Type Ambiguity in Practice
A union is ambiguous when the same configuration object could match more than one member, for example two structs that differ only in optional fields.
With the default allowTypeAmbiguity: false, ambiguous input is rejected, and the generated schemas require the $type discriminator as a constant per member, so deserialization
is always unambiguous.
With allowTypeAmbiguity: true, values match structurally: $type is still generated as a constant but the configuration author may omit it.
Overlapping members are then accepted, which makes the behavior dependent on matching order and harder to maintain; different consumers can parse the same untagged value
differently.
Avoid it unless the overlap is the point.
Number members stored in different units are a warning: the exported number no longer tells which unit it is in.
Reference
A ref points at an entity configured elsewhere:
monitoredSensorRef: {
description: "The sensor supervised by the plausibility check.",
ref: {
refTargetTypes: [
"SensorEcu::Ecu::Sensor"
]
}
},
refTargetTypes lists the type paths the target entity may have.
In the configuration, the reference value takes one of two forms:
| Form | Example |
|---|---|
Absolute path of entity names |
|
Id reference |
|
monitoredSensorRef: "/SensorEcu/FrontBodyEcu/OilTemp",
The path segments are the configured entity identities (name, else id), starting at the definition name.
They are not the definition’s relation names.
A reference can also be written as an object to attach documentation; this one addresses its target with the id: form:
failsafeSensorRef: {
refTarget: "id:ignition",
$description: "Fall back to the ignition signal when the monitored sensor fails."
}
References are validated: the target must exist, be an entity, and its type must be one of the refTargetTypes.
Named Type Reference
typeRef is not a data type but a reuse mechanism: the element takes the shape of a named type declared elsewhere.
See Reusable types and inheritance, which also covers the recursive case.
inputFilter: {
description: "Filter applied before scaling.",
typeRef: "SensorEcu::Filter"
},
outputFilter: {
description: "Filter applied after scaling.",
typeRef: "SensorEcu::Filter"
},