Skip to content

clarotech/openEHR-json

Repository files navigation

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);

Links and FeederAudit on Locatables

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

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages