Clarotech.OpenEHR.RM.Json 1.0.0

dotnet add package Clarotech.OpenEHR.RM.Json --version 1.0.0
                    
NuGet\Install-Package Clarotech.OpenEHR.RM.Json -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Clarotech.OpenEHR.RM.Json" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Clarotech.OpenEHR.RM.Json" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Clarotech.OpenEHR.RM.Json" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Clarotech.OpenEHR.RM.Json --version 1.0.0
                    
#r "nuget: Clarotech.OpenEHR.RM.Json, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Clarotech.OpenEHR.RM.Json@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Clarotech.OpenEHR.RM.Json&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Clarotech.OpenEHR.RM.Json&version=1.0.0
                    
Install as a Cake Tool

Clarotech.OpenEHR.RM.Json

Canonical JSON serialization and deserialization for the openEHR Reference Model using System.Text.Json.

Faithfully implements the openEHR canonical JSON format: _type discriminator written first on every object, snake_case property names, optional properties omitted when null. Covers every concrete RM type in the openEHR RM 1.1.0 specification — compositions, data types, data structures, EHR records, and versioning.

NuGet CI License

Contents


Installation

dotnet add package Clarotech.OpenEHR.RM.Json

Targets net8.0 and net10.0.


API overview

using Clarotech.OpenEHR.RM.Json;
Member Description
OpenEhrJsonSerializer.Serialize<T>(value) Serialize to canonical JSON string (compact)
OpenEhrJsonSerializer.Serialize<T>(value, writeIndented: true) Serialize to canonical JSON string (pretty-printed)
OpenEhrJsonSerializer.SerializeByRuntimeType(value) Serialize using the object's runtime type — use when the declared type is abstract
OpenEhrJsonSerializer.Deserialize<T>(json) Deserialize from a JSON string
OpenEhrJsonSerializer.Deserialize<T>(stream) Deserialize synchronously from a UTF-8 stream
OpenEhrJsonSerializer.DeserializeAsync<T>(stream, ct) Deserialize asynchronously from a UTF-8 stream
OpenEhrJsonOptions.Default The pre-configured JsonSerializerOptions — use directly in ASP.NET, HttpClient, etc.

Serialization

Compact (default)

string json = OpenEhrJsonSerializer.Serialize(composition);
// {"_type":"COMPOSITION","archetype_node_id":"openEHR-EHR-COMPOSITION.encounter.v1",...}

Pretty-printed

string json = OpenEhrJsonSerializer.Serialize(composition, writeIndented: true);
// {
//   "_type": "COMPOSITION",
//   "archetype_node_id": "openEHR-EHR-COMPOSITION.encounter.v1",
//   ...
// }

Serializing via a base/abstract type variable

When your variable is declared as a base type (e.g. DataValue, ContentItem, ItemStructure), use SerializeByRuntimeType to ensure the correct _type discriminator is written:

DataValue value = new DvQuantity(72.5, "kg");

// Wrong: serialises as DataValue — no _type written
string bad  = OpenEhrJsonSerializer.Serialize(value);

// Correct: uses DvQuantity's runtime type
string good = OpenEhrJsonSerializer.SerializeByRuntimeType(value);
// {"_type":"DV_QUANTITY","magnitude":72.5,"units":"kg"}

Deserialization

From a string

Composition? comp = OpenEhrJsonSerializer.Deserialize<Composition>(json);

Deserializing a polymorphic base type

The _type discriminator in the JSON drives dispatch — you can deserialize any subtype through the base:

string json = """{"_type":"DV_CODED_TEXT","value":"Blood pressure","defining_code":{...}}""";

DataValue? dv = OpenEhrJsonSerializer.Deserialize<DataValue>(json);
// dv is DvCodedText at runtime

From a Stream

using var stream = File.OpenRead("composition.json");
Composition? comp = OpenEhrJsonSerializer.Deserialize<Composition>(stream);

Asynchronously from a Stream

using var stream = File.OpenRead("composition.json");
Composition? comp = await OpenEhrJsonSerializer.DeserializeAsync<Composition>(stream, cancellationToken);

Using OpenEhrJsonOptions.Default directly

OpenEhrJsonOptions.Default is a pre-configured, shared JsonSerializerOptions instance. Use it wherever the framework expects options — no need to create converters manually.

JsonSerializerOptions opts = OpenEhrJsonOptions.Default;

// Plain System.Text.Json calls
string json = JsonSerializer.Serialize(composition, opts);
Composition? comp = JsonSerializer.Deserialize<Composition>(json, opts);

Worked examples

Composition

Build a minimal COMPOSITION and serialize it:

using OpenEHR.RM.Composition;
using OpenEHR.RM.DataTypes.Text;
using OpenEHR.RM.DataTypes.Basic;
using OpenEHR.RM.Common.Generic;

var composition = new Composition
{
    ArchetypeNodeId = "openEHR-EHR-COMPOSITION.encounter.v1",
    Name            = new DvText("Encounter"),
    Language        = new CodePhrase(new TerminologyId("ISO_639-1"),  "en"),
    Territory       = new CodePhrase(new TerminologyId("ISO_3166-1"), "GB"),
    Category        = new DvCodedText("event", new CodePhrase(new TerminologyId("openehr"), "433")),
    Composer        = new PartySelf(),
    ArchetypeDetails = new Archetyped
    {
        ArchetypeId  = new ArchetypeId("openEHR-EHR-COMPOSITION.encounter.v1"),
        RmVersion    = "1.1.0",
        TemplateId   = new TemplateId("Encounter")
    }
};

string json = OpenEhrJsonSerializer.Serialize(composition, writeIndented: true);

Output:

{
  "_type": "COMPOSITION",
  "archetype_node_id": "openEHR-EHR-COMPOSITION.encounter.v1",
  "name": {
    "_type": "DV_TEXT",
    "value": "Encounter"
  },
  "archetype_details": {
    "_type": "ARCHETYPED",
    "archetype_id": { "_type": "ARCHETYPE_ID", "value": "openEHR-EHR-COMPOSITION.encounter.v1" },
    "template_id":  { "_type": "TEMPLATE_ID",  "value": "Encounter" },
    "rm_version": "1.1.0"
  },
  "language":  { "_type": "CODE_PHRASE", "terminology_id": { "_type": "TERMINOLOGY_ID", "value": "ISO_639-1"  }, "code_string": "en" },
  "territory": { "_type": "CODE_PHRASE", "terminology_id": { "_type": "TERMINOLOGY_ID", "value": "ISO_3166-1" }, "code_string": "GB" },
  "category": {
    "_type": "DV_CODED_TEXT",
    "value": "event",
    "defining_code": { "_type": "CODE_PHRASE", "terminology_id": { "_type": "TERMINOLOGY_ID", "value": "openehr" }, "code_string": "433" }
  },
  "composer": { "_type": "PARTY_SELF" }
}

Round-trip:

Composition? restored = OpenEhrJsonSerializer.Deserialize<Composition>(json);
Console.WriteLine(restored!.Language.CodeString); // "en"
Console.WriteLine(restored.Composer is PartySelf); // true

Data types

DV_TEXT and DV_CODED_TEXT
var plain = new DvText("Blood pressure");
// {"_type":"DV_TEXT","value":"Blood pressure"}

var coded = new DvCodedText(
    "Blood pressure",
    new CodePhrase(new TerminologyId("LOINC"), "55284-4"));
// {"_type":"DV_CODED_TEXT","value":"Blood pressure","defining_code":{...}}

// DvText with hyperlink formatting
var linked = new DvText("See guidelines", formatting: "plain", hyperlink: new DvUri("https://example.com"));
// {"_type":"DV_TEXT","value":"See guidelines","formatting":"plain","hyperlink":{"_type":"DV_URI","value":"https://example.com"}}
DV_QUANTITY
var qty = new DvQuantity(72.5, "kg");
// {"_type":"DV_QUANTITY","magnitude":72.5,"units":"kg"}

// With precision and units system
var detailed = new DvQuantity(
    magnitude:        36.6,
    units:            "Cel",
    precision:        1,
    unitsSystem:      "UCUM",
    unitsDisplayName: "°C");
// {"_type":"DV_QUANTITY","magnitude":36.6,"units":"Cel","precision":1,"units_system":"UCUM","units_display_name":"°C"}

// With accuracy
var withAccuracy = new DvQuantity(
    magnitude:        120.0,
    units:            "mm[Hg]",
    accuracy:         2.0,
    accuracyIsPercent: false);
DV_COUNT
var count = new DvCount(3);
// {"_type":"DV_COUNT","magnitude":3}
DV_PROPORTION
var ratio = new DvProportion(1.0, 4.0, ProportionKind.Ratio);
// {"_type":"DV_PROPORTION","numerator":1,"denominator":4,"type":0}

var percent = new DvProportion(95.0, 100.0, ProportionKind.Percentage, precision: 1);
// {"_type":"DV_PROPORTION","numerator":95,"denominator":100,"type":2,"precision":1}
DV_ORDINAL and DV_SCALE
var ordinal = new DvOrdinal(
    2,
    new DvCodedText("Moderate", new CodePhrase(new TerminologyId("local"), "at0006")));
// {"_type":"DV_ORDINAL","value":2,"symbol":{"_type":"DV_CODED_TEXT","value":"Moderate",...}}

var scale = new DvScale(
    2.5,
    new DvCodedText("Mild–moderate", new CodePhrase(new TerminologyId("local"), "at0007")));
DV_BOOLEAN
var flag = new DvBoolean(true);
// {"_type":"DV_BOOLEAN","value":true}
DV_DATE_TIME, DV_DATE, DV_TIME
var dt   = new DvDateTime("2024-11-15T09:30:00Z");
var date = new DvDate("2024-11-15");
var time = new DvTime("09:30:00Z");
// {"_type":"DV_DATE_TIME","value":"2024-11-15T09:30:00Z"}
// {"_type":"DV_DATE","value":"2024-11-15"}
// {"_type":"DV_TIME","value":"09:30:00Z"}
DV_DURATION
var dur = new DvDuration("PT2H30M");
// {"_type":"DV_DURATION","value":"PT2H30M"}
DV_IDENTIFIER
var id = new DvIdentifier(
    id:       "ABC123",
    issuer:   "NHS",
    assigner: "GP Practice",
    type:     "LOCAL");
// {"_type":"DV_IDENTIFIER","id":"ABC123","issuer":"NHS","assigner":"GP Practice","type":"LOCAL"}
DV_URI and DV_EHR_URI
var uri    = new DvUri("https://example.com/resource");
var ehrUri = new DvEhrUri("ehr://system.example.com/ehr/abc123");
DV_MULTIMEDIA
var media = new DvMultimedia(
    mediaType:   new CodePhrase(new TerminologyId("IANA_media-types"), "image/png"),
    size:        2048,
    uri:         new DvUri("https://example.com/image.png"));
// {"_type":"DV_MULTIMEDIA","media_type":{...},"size":2048,"uri":{...}}
DV_PARSABLE
var parsable = new DvParsable("text/plain", "Some structured text");
// {"_type":"DV_PARSABLE","formalism":"text/plain","value":"Some structured text"}

DvOrdered with normal range and reference ranges

All DvOrdered subtypes support NormalStatus, NormalRange, and OtherReferenceRanges.

// Normal range: systolic blood pressure 90–140 mmHg
var normalRange = new ReferenceRange<DvOrdered>(
    range:   new DvInterval<DvOrdered>(
                 lower:          new DvQuantity(90,  "mm[Hg]"),
                 upper:          new DvQuantity(140, "mm[Hg]"),
                 lowerUnbounded: false,
                 upperUnbounded: false,
                 lowerIncluded:  true,
                 upperIncluded:  true),
    meaning: new DvText("Normal adult range"));

// Additional reference ranges
var pediatricRange = new ReferenceRange<DvOrdered>(
    range:   new DvInterval<DvOrdered>(
                 lower:          new DvQuantity(80,  "mm[Hg]"),
                 upper:          new DvQuantity(120, "mm[Hg]"),
                 lowerUnbounded: false,
                 upperUnbounded: false,
                 lowerIncluded:  true,
                 upperIncluded:  true),
    meaning: new DvText("Pediatric range"));

var systolic = new DvQuantity(
    magnitude:             125.0,
    units:                 "mm[Hg]",
    normalStatus:          new CodePhrase(new TerminologyId("openehr_normal_statuses"), "N"),
    normalRange:           normalRange,
    otherReferenceRanges:  new[] { pediatricRange });

string json = OpenEhrJsonSerializer.Serialize(systolic, writeIndented: true);
// {
//   "_type": "DV_QUANTITY",
//   "magnitude": 125,
//   "units": "mm[Hg]",
//   "normal_status": { "_type": "CODE_PHRASE", ... "code_string": "N" },
//   "normal_range": {
//     "_type": "REFERENCE_RANGE",
//     "meaning": { "_type": "DV_TEXT", "value": "Normal adult range" },
//     "range": { "_type": "DV_INTERVAL", "lower": { "_type": "DV_QUANTITY", ... }, ... }
//   },
//   "other_reference_ranges": [...]
// }

// Deserialize and access the normal range
DvQuantity? restored = OpenEhrJsonSerializer.Deserialize<DvQuantity>(json);
Console.WriteLine(restored!.NormalRange?.Meaning?.Value); // "Normal adult range"
Console.WriteLine(restored.NormalStatus?.CodeString);     // "N"

DvInterval

DvInterval<T> wraps a bounded range of any DvOrdered subtype. It always carries lower_included, lower_unbounded, upper_included, upper_unbounded.

// Closed interval: 18–65 years
var ageRange = new DvInterval<DvQuantity>(
    lower:          new DvQuantity(18, "a"),
    upper:          new DvQuantity(65, "a"),
    lowerUnbounded: false,
    upperUnbounded: false,
    lowerIncluded:  true,
    upperIncluded:  true);

// Upper-unbounded: ≥ 18
var adultRange = new DvInterval<DvQuantity>(
    lower:          new DvQuantity(18, "a"),
    upper:          null,
    lowerUnbounded: false,
    upperUnbounded: true,
    lowerIncluded:  true,
    upperIncluded:  false);

// Interval of date-times (e.g. participation window)
var window = new DvInterval<DvDateTime>(
    lower:          new DvDateTime("2024-01-01T00:00:00Z"),
    upper:          new DvDateTime("2024-12-31T23:59:59Z"),
    lowerUnbounded: false,
    upperUnbounded: false,
    lowerIncluded:  true,
    upperIncluded:  true);

string json = OpenEhrJsonSerializer.Serialize(ageRange, writeIndented: true);
// {
//   "_type": "DV_INTERVAL",
//   "lower": { "_type": "DV_QUANTITY", "magnitude": 18, "units": "a" },
//   "lower_included": true,
//   "lower_unbounded": false,
//   "upper": { "_type": "DV_QUANTITY", "magnitude": 65, "units": "a" },
//   "upper_included": true,
//   "upper_unbounded": false
// }

DvInterval<DvQuantity>? restored = OpenEhrJsonSerializer.Deserialize<DvInterval<DvQuantity>>(json);
Console.WriteLine(restored!.Lower?.Magnitude); // 18
Console.WriteLine(restored.UpperUnbounded);    // false

Data structures — ITEM_TREE, CLUSTER, ELEMENT

var tree = new ItemTree
{
    ArchetypeNodeId = "at0001",
    Name            = new DvText("Tree"),
    Items           = new List<Item>
    {
        new Cluster
        {
            ArchetypeNodeId = "at0002",
            Name            = new DvText("Blood pressure"),
            Items           = new List<Item>
            {
                new Element
                {
                    ArchetypeNodeId = "at0003",
                    Name            = new DvText("Systolic"),
                    Value           = new DvQuantity(125.0, "mm[Hg]")
                },
                new Element
                {
                    ArchetypeNodeId = "at0004",
                    Name            = new DvText("Diastolic"),
                    Value           = new DvQuantity(82.0, "mm[Hg]")
                }
            }
        },
        new Element
        {
            ArchetypeNodeId = "at0005",
            Name            = new DvText("Comment"),
            Value           = new DvText("Patient was resting")
        }
    }
};

string json = OpenEhrJsonSerializer.Serialize(tree, writeIndented: true);

An ELEMENT with no recorded value uses NullFlavour:

var missing = new Element
{
    ArchetypeNodeId = "at0006",
    Name            = new DvText("Body weight"),
    NullFlavour     = new DvCodedText(
                          "no information",
                          new CodePhrase(new TerminologyId("openehr"), "271")),
    NullReason      = new DvText("Not measured today")
};

An ITEM_TABLE represents rows as CLUSTER objects:

var table = new ItemTable
{
    ArchetypeNodeId = "at0001",
    Name            = new DvText("Medications"),
    Rows            = new List<Cluster>
    {
        new Cluster
        {
            ArchetypeNodeId = "at0002",
            Name            = new DvText("Row 1"),
            Items           = new List<Item>
            {
                new Element { ArchetypeNodeId = "at0003", Name = new DvText("Name"),  Value = new DvText("Metformin") },
                new Element { ArchetypeNodeId = "at0004", Name = new DvText("Dose"),  Value = new DvQuantity(500, "mg") }
            }
        }
    }
};
// {"_type":"ITEM_TABLE","archetype_node_id":"at0001","name":{...},"rows":[...]}

HISTORY and events

var history = new History<ItemTree>
{
    ArchetypeNodeId = "at0001",
    Name            = new DvText("History"),
    Origin          = new DvDateTime("2024-11-15T09:30:00Z"),
    Events          = new List<Event<ItemTree>>
    {
        new PointEvent<ItemTree>
        {
            ArchetypeNodeId = "at0002",
            Name            = new DvText("Any event"),
            Time            = new DvDateTime("2024-11-15T09:30:00Z"),
            Data            = tree
        }
    }
};
// Note: HISTORY objects do NOT carry a "_type" discriminator in canonical JSON.
// Point events carry "_type":"POINT_EVENT".

An INTERVAL_EVENT adds width and math_function:

var intervalEvent = new IntervalEvent<ItemTree>
{
    ArchetypeNodeId = "at0002",
    Name            = new DvText("24-hour mean"),
    Time            = new DvDateTime("2024-11-15T09:00:00Z"),
    Data            = tree,
    Width           = new DvDuration("PT24H"),
    MathFunction    = new DvCodedText(
                          "mean",
                          new CodePhrase(new TerminologyId("openehr"), "146")),
    SampleCount     = 96
};

OBSERVATION with full protocol and state

var observation = new Observation
{
    ArchetypeNodeId = "openEHR-EHR-OBSERVATION.blood_pressure.v2",
    Name            = new DvText("Blood pressure"),
    Language        = new CodePhrase(new TerminologyId("ISO_639-1"),  "en"),
    Encoding        = new CodePhrase(new TerminologyId("IANA_character-sets"), "UTF-8"),
    Subject         = new PartySelf(),
    Provider        = new PartyIdentified(name: "Dr Smith"),
    Data            = new History<ItemStructure>
    {
        ArchetypeNodeId = "at0001",
        Name            = new DvText("History"),
        Origin          = new DvDateTime("2024-11-15T09:30:00Z"),
        Events          = new List<Event<ItemStructure>>
        {
            new PointEvent<ItemStructure>
            {
                ArchetypeNodeId = "at0002",
                Name            = new DvText("Any event"),
                Time            = new DvDateTime("2024-11-15T09:30:00Z"),
                Data            = tree
            }
        }
    },
    Protocol = new ItemTree
    {
        ArchetypeNodeId = "at0011",
        Name            = new DvText("Protocol"),
        Items           = new List<Item>
        {
            new Element
            {
                ArchetypeNodeId = "at0013",
                Name            = new DvText("Cuff size"),
                Value           = new DvCodedText(
                                      "Adult",
                                      new CodePhrase(new TerminologyId("local"), "at0015"))
            }
        }
    }
};

string json = OpenEhrJsonSerializer.Serialize(observation);
Observation? restored = OpenEhrJsonSerializer.Deserialize<Observation>(json);

Versioning — OriginalVersion and Contribution

using OpenEHR.RM.Common.ChangeControl;
using OpenEHR.RM.Common.Generic;
using OpenEHR.RM.Support.Identification;

// Create an OriginalVersion wrapping a Composition
var version = new OriginalVersion<Composition>
{
    Uid          = new ObjectVersionId("abc123::system.example.com::1"),
    Contribution = new ObjectRef
    {
        Id        = new HierObjectId("contrib-uid-001"),
        Namespace = "local",
        Type      = "CONTRIBUTION"
    },
    CommitAudit  = new AuditDetails
    {
        SystemId      = "system.example.com",
        Committer     = new PartyIdentified(name: "Dr Smith"),
        TimeCommitted = new DvDateTime("2024-11-15T09:35:00Z"),
        ChangeType    = new DvCodedText(
                            "creation",
                            new CodePhrase(new TerminologyId("openehr"), "249"))
    },
    LifecycleState = new DvCodedText(
                         "complete",
                         new CodePhrase(new TerminologyId("openehr"), "532")),
    Data = composition,
    PrecedingVersionUid = null   // first version
};

string versionJson = OpenEhrJsonSerializer.Serialize(version, writeIndented: true);
// {
//   "_type": "ORIGINAL_VERSION",
//   "uid": { "_type": "OBJECT_VERSION_ID", "value": "abc123::system.example.com::1" },
//   "contribution": { "_type": "OBJECT_REF", ... },
//   "commit_audit": { "_type": "AUDIT_DETAILS", ... },
//   "lifecycle_state": { "_type": "DV_CODED_TEXT", ... },
//   "data": { "_type": "COMPOSITION", ... }
// }

// A Contribution groups one or more version references
var contribution = new Contribution
{
    Uid      = new HierObjectId("contrib-uid-001"),
    Audit    = version.CommitAudit,
    Versions = new HashSet<ObjectRef>
    {
        new ObjectRef
        {
            Id        = new ObjectVersionId("abc123::system.example.com::1"),
            Namespace = "local",
            Type      = "VERSIONED_COMPOSITION"
        }
    }
};

// Deserialize
OriginalVersion<Composition>? restored =
    OpenEhrJsonSerializer.Deserialize<OriginalVersion<Composition>>(versionJson);

Console.WriteLine(restored!.Uid.Value);                       // "abc123::system.example.com::1"
Console.WriteLine(restored.LifecycleState.DefiningCode.CodeString); // "532"

An ImportedVersion wraps a pre-existing OriginalVersion imported from another system:

var imported = new ImportedVersion<Composition>
{
    CommitAudit  = auditDetails,
    Contribution = contributionRef,
    Item         = version         // the wrapped OriginalVersion
};
// {"_type":"IMPORTED_VERSION","contribution":{...},"commit_audit":{...},"item":{...}}

EHR record and EHR_STATUS

using OpenEHR.RM.Ehr;

// EHR_STATUS (a Locatable — carries the modifiability flags and subject)
var ehrStatus = new EhrStatus
{
    ArchetypeNodeId = "openEHR-EHR-EHR_STATUS.generic.v1",
    Name            = new DvText("EHR Status"),
    Subject         = new PartySelf
    {
        ExternalRef = new PartyRef
        {
            Id        = new HierObjectId("patient-uuid-001"),
            Namespace = "local",
            Type      = "PERSON"
        }
    },
    IsModifiable = true,
    IsQueryable  = true,
    OtherDetails = new ItemTree
    {
        ArchetypeNodeId = "at0001",
        Name            = new DvText("Details"),
        Items           = new List<Item>
        {
            new Element
            {
                ArchetypeNodeId = "at0002",
                Name  = new DvText("Opt-in flag"),
                Value = new DvBoolean(true)
            }
        }
    }
};

string statusJson = OpenEhrJsonSerializer.Serialize(ehrStatus, writeIndented: true);

EhrStatus? restoredStatus = OpenEhrJsonSerializer.Deserialize<EhrStatus>(statusJson);
Console.WriteLine(restoredStatus!.IsModifiable); // true
Console.WriteLine((restoredStatus.Subject.ExternalRef as PartyRef)!.Id.Value); // "patient-uuid-001"

// EHR root record — all composition/contribution fields are OBJECT_REF references
var ehr = new Ehr
{
    EhrId       = new HierObjectId("ehr-uuid-001"),
    SystemId    = new HierObjectId("system.example.com"),
    EhrStatus   = new ObjectRef { Id = new HierObjectId("status-uid"), Namespace = "local", Type = "EHR_STATUS" },
    EhrAccess   = new ObjectRef { Id = new HierObjectId("access-uid"), Namespace = "local", Type = "EHR_ACCESS" },
    TimeCreated = new DvDateTime("2024-01-01T00:00:00Z"),
    Compositions = new List<ObjectRef>
    {
        new ObjectRef { Id = new HierObjectId("comp-uid-001"), Namespace = "local", Type = "VERSIONED_COMPOSITION" }
    }
};

string ehrJson = OpenEhrJsonSerializer.Serialize(ehr, writeIndented: true);

Every Locatable (compositions, entries, data structures, items) can carry Links and a FeederAudit.

// Links — typed directed references to other RM objects
var link = new Link
{
    Meaning = new DvText("has-subject"),
    Type    = new DvText("PROBLEM"),
    Target  = new DvEhrUri("ehr://system.example.com/ehr/abc/compositions/xyz")
};

var element = new Element
{
    ArchetypeNodeId = "at0001",
    Name            = new DvText("Problem"),
    Value           = new DvText("Hypertension"),
    Links           = new HashSet<Link> { link }
};

// FeederAudit — provenance of data imported from a feeder system
var feederAudit = new FeederAudit
{
    OriginatingSystemAudit = new FeederAuditDetails
    {
        SystemId  = "GP-System-v3",
        Time      = new DvDateTime("2024-11-14T08:00:00Z"),
        VersionId = "REV-00123"
    },
    OriginatingSystemItemIds = new List<DvIdentifier>
    {
        new DvIdentifier(id: "GP-OBS-9876", issuer: "GP-System-v3", type: "LOCAL")
    }
};

var observation = new Observation
{
    // ... other fields ...
    FeederAudit = feederAudit
};

Integrations

ASP.NET Core

Configure the JSON serializer once at startup so all controllers serialize and deserialize canonical openEHR JSON:

// Program.cs / Startup.cs
builder.Services.AddControllers()
    .AddJsonOptions(o =>
    {
        // Copy all openEHR converters into the ASP.NET JsonSerializerOptions
        foreach (var converter in OpenEhrJsonOptions.Default.Converters)
            o.JsonSerializerOptions.Converters.Add(converter);

        o.JsonSerializerOptions.PropertyNamingPolicy        = JsonNamingPolicy.SnakeCaseLower;
        o.JsonSerializerOptions.DefaultIgnoreCondition      = JsonIgnoreCondition.WhenWritingNull;
        o.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
    });

Or pass OpenEhrJsonOptions.Default to JsonContent / JsonSerializer in individual endpoints.

HttpClient / HttpContent

using System.Net.Http.Json;

// POST a composition
var content = JsonContent.Create(composition, options: OpenEhrJsonOptions.Default);
HttpResponseMessage response = await httpClient.PostAsync("/ehr/{ehr_id}/compositions", content);

// GET and deserialize
Composition? comp = await httpClient.GetFromJsonAsync<Composition>(
    "/ehr/{ehr_id}/compositions/{version_uid}",
    OpenEhrJsonOptions.Default);

Stream deserialization

// Synchronous (e.g., reading a local file)
using var stream = File.OpenRead("composition.json");
Composition? comp = OpenEhrJsonSerializer.Deserialize<Composition>(stream);

// Asynchronous (e.g., reading an HTTP response stream)
using var responseStream = await httpResponse.Content.ReadAsStreamAsync();
Composition? comp = await OpenEhrJsonSerializer.DeserializeAsync<Composition>(
    responseStream, cancellationToken);

Supported RM types

Data types

openEHR type .NET class Notes
DV_TEXT DvText Optional: formatting, hyperlink, language, encoding, mappings
DV_CODED_TEXT DvCodedText Extends DV_TEXT; adds defining_code
DV_PARAGRAPH DvParagraph Deprecated in RM 1.1.0
DV_QUANTITY DvQuantity magnitude, units; optional: precision, units_system, units_display_name, magnitude_status, accuracy
DV_COUNT DvCount Integer magnitude
DV_PROPORTION DvProportion numerator, denominator, type (enum ProportionKind)
DV_ORDINAL DvOrdinal value (int) + symbol (DvCodedText)
DV_SCALE DvScale value (double) + symbol (DvCodedText)
DV_BOOLEAN DvBoolean
DV_DATE_TIME DvDateTime ISO 8601 string
DV_DATE DvDate
DV_TIME DvTime
DV_DURATION DvDuration ISO 8601 period string
DV_IDENTIFIER DvIdentifier
DV_URI DvUri
DV_EHR_URI DvEhrUri
DV_MULTIMEDIA DvMultimedia
DV_PARSABLE DvParsable
DV_INTERVAL DvInterval<T> Generic; bounds carry _type discriminators
REFERENCE_RANGE ReferenceRange<T> Generic; available on all DvOrdered subtypes

All DvOrdered subtypes (DV_QUANTITY, DV_COUNT, DV_PROPORTION, DV_ORDINAL, DV_SCALE, DV_DATE_TIME, DV_DATE, DV_TIME, DV_DURATION) fully support normal_status, normal_range, and other_reference_ranges.

Data structures

openEHR type .NET class
ITEM_TREE ItemTree
ITEM_LIST ItemList
ITEM_SINGLE ItemSingle
ITEM_TABLE ItemTable
CLUSTER Cluster
ELEMENT Element
HISTORY History<T>
POINT_EVENT PointEvent<T>
INTERVAL_EVENT IntervalEvent<T>

Composition and entries

openEHR type .NET class
COMPOSITION Composition
SECTION Section
OBSERVATION Observation
EVALUATION Evaluation
INSTRUCTION Instruction
ACTION Action
ADMIN_ENTRY AdminEntry

Common / support

openEHR type .NET class
PARTY_SELF PartySelf
PARTY_IDENTIFIED PartyIdentified
PARTY_RELATED PartyRelated
PARTICIPATION Participation
AUDIT_DETAILS AuditDetails
ATTESTATION Attestation
REVISION_HISTORY RevisionHistory
REVISION_HISTORY_ITEM RevisionHistoryItem
LINK Link
FEEDER_AUDIT FeederAudit
FEEDER_AUDIT_DETAILS FeederAuditDetails
OBJECT_REF ObjectRef
PARTY_REF PartyRef
LOCATABLE_REF LocatableRef
HIER_OBJECT_ID HierObjectId
OBJECT_VERSION_ID ObjectVersionId
GENERIC_ID GenericId
ARCHETYPE_ID ArchetypeId
TEMPLATE_ID TemplateId
ARCHETYPED Archetyped

Versioning

openEHR type .NET class
ORIGINAL_VERSION OriginalVersion<T>
IMPORTED_VERSION ImportedVersion<T>
CONTRIBUTION Contribution

EHR

openEHR type .NET class
EHR Ehr
EHR_STATUS EhrStatus
EHR_ACCESS EhrAccess
VERSIONED_COMPOSITION VersionedComposition
VERSIONED_EHR_STATUS VersionedEhrStatus
VERSIONED_EHR_ACCESS VersionedEhrAccess

Dependencies


License

Apache-2.0 — see LICENSE.txt

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 116 5/25/2026