JSON Serialisation — Introduction
openEHR Canonical JSON
The openEHR REST API Specification
defines a Canonical JSON encoding for all RM objects. Clarotech.OpenEHR.RM.Json
implements this encoding faithfully. The key rules are:
_typediscriminator — every serialised RM object carries a"_type"field with the openEHR RM type name (e.g."DV_QUANTITY","COMPOSITION","OBSERVATION"). This is written as the first property of each JSON object so consumers can resolve the concrete type before reading remaining fields.snake_caseproperty names — all property names aresnake_case(e.g.archetype_node_id,defining_code). This is enforced viaJsonNamingPolicy.SnakeCaseLower.- Null omission — properties with
nullvalues are omitted entirely from the output (DefaultIgnoreCondition = WhenWritingNull). - Case-insensitive deserialisation — the deserialiser accepts both
snake_caseandcamelCaseproperty names, making it tolerant of minor format variations from third-party openEHR CDRs.
The two public types
OpenEhrJsonSerializer
The static entry point for all serialisation and deserialisation. Three method groups:
| Method | Description |
|---|---|
Serialize<T>(T, bool) |
Serialise using the declared type T |
SerializeByRuntimeType(object, bool) |
Serialise using the runtime type — use this when the declared type is abstract (e.g. DataValue) to ensure the correct _type is emitted |
Deserialize<T>(string) |
Deserialise from a JSON string |
Deserialize<T>(Stream) |
Deserialise from a UTF-8 byte stream |
DeserializeAsync<T>(Stream, CancellationToken) |
Async stream deserialisation |
OpenEhrJsonOptions
JsonSerializerOptions opts = OpenEhrJsonOptions.Default;
OpenEhrJsonOptions.Default is a thread-safe, shared JsonSerializerOptions instance
pre-loaded with converters for every type in the RM hierarchy. You can layer additional
converters on top:
var custom = new JsonSerializerOptions(OpenEhrJsonOptions.Default);
custom.Converters.Add(new MyCustomConverter());
string json = JsonSerializer.Serialize(value, custom);
Converter architecture
Converters are organised to mirror the RM package structure:
Converters/
├── DataValueConverter.cs — polymorphic root; reads "_type" to dispatch
├── DataTypes/ — DvText, DvCodedText, DvQuantity, DvCount,
│ DvProportion, DvOrdinal, DvBoolean,
│ DvDateTime/Date/Time/Duration,
│ DvIdentifier, DvUri, DvEhrUri,
│ DvMultimedia, DvParsable
├── Common/ — PartyProxy (→ Identified/Self/Related),
│ AuditDetails, Attestation, Link,
│ FeederAudit, RevisionHistory
├── DataStructures/ — ItemStructure, History<T>, Event<T>,
│ ItemTree/List/Table/Single, Cluster, Element
├── Composition/ — Composition, Section, ContentItem,
│ Observation, Evaluation, Instruction,
│ Action, AdminEntry, EventContext
├── ChangeControl/ — Version<T>, OriginalVersion<T>,
│ ImportedVersion<T>, Contribution
├── Ehr/ — Ehr, EhrStatus, EhrAccess,
│ VersionedComposition, VersionedEhrStatus
└── Support/ — UidBasedId, ObjectId, ObjectRef
Trim / AOT note
All converters use reflection and are annotated with [RequiresUnreferencedCode].
The library is not safe for use with PublishTrimmed=true or Native AOT publishing
without a custom source-generated serialiser context. If you need AOT support, open an
issue on clarotech/openEHR-json.