Serialisation

dotnet add package Clarotech.OpenEHR.RM.Json
using Clarotech.OpenEHR.RM.Json;

Basic usage — Serialize<T>

// Compact (default)
string json = OpenEhrJsonSerializer.Serialize(composition);

// Pretty-printed
string pretty = OpenEhrJsonSerializer.Serialize(composition, writeIndented: true);

Use Serialize<T> when the declared type of your variable already is a concrete RM class (e.g. Composition, DvQuantity). The _type discriminator is derived from T.

Serialising abstract-typed variables — SerializeByRuntimeType

When your variable is declared as an abstract or interface type (e.g. DataValue, ContentItem, ItemStructure) you must use SerializeByRuntimeType to ensure the correct _type is written:

DataValue value = element.Value;   // runtime type: DvQuantity

// ❌ Wrong — emits "_type":"DATA_VALUE" (base class name)
string wrong = OpenEhrJsonSerializer.Serialize(value);

// ✅ Correct — emits "_type":"DV_QUANTITY"
string correct = OpenEhrJsonSerializer.SerializeByRuntimeType(value);

Example output

Given the blood-pressure Composition from Building a Composition:

{
  "_type": "COMPOSITION",
  "name": {
    "_type": "DV_TEXT",
    "value": "Vital Signs"
  },
  "archetype_node_id": "openEHR-EHR-COMPOSITION.encounter.v1",
  "archetype_details": {
    "_type": "ARCHETYPED",
    "archetype_id": {
      "_type": "ARCHETYPE_ID",
      "value": "openEHR-EHR-COMPOSITION.encounter.v1"
    },
    "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_IDENTIFIED",
    "name": "Dr Alice Smith"
  },
  "context": {
    "_type": "EVENT_CONTEXT",
    "start_time": { "_type": "DV_DATE_TIME", "value": "2025-05-25T09:15:00+01:00" },
    "setting": {
      "_type": "DV_CODED_TEXT",
      "value": "primary medical care",
      "defining_code": {
        "_type": "CODE_PHRASE",
        "terminology_id": { "_type": "TERMINOLOGY_ID", "value": "openehr" },
        "code_string": "228"
      }
    }
  },
  "content": [ … ]
}

Note — Null-valued properties are omitted. Optional properties that were not set (e.g. uid, links, feeder_audit) do not appear in the output.

Using OpenEhrJsonOptions directly

You can pass OpenEhrJsonOptions.Default directly to System.Text.Json APIs:

// Write to a stream (e.g. HttpResponseBody)
await JsonSerializer.SerializeAsync(responseStream, composition, OpenEhrJsonOptions.Default);

// Wrap in custom options
var opts = new JsonSerializerOptions(OpenEhrJsonOptions.Default) { WriteIndented = true };
string json = JsonSerializer.Serialize(composition, opts);

Writing to a file

string json = OpenEhrJsonSerializer.Serialize(composition, writeIndented: true);
await File.WriteAllTextAsync("vital-signs.json", json, Encoding.UTF8);

Posting to an openEHR CDR

using var client = new HttpClient { BaseAddress = new Uri("https://cdr.hospital.example/ehrbase/") };
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

string json = OpenEhrJsonSerializer.Serialize(composition);
var content = new StringContent(json, Encoding.UTF8, "application/json");

var response = await client.PostAsync($"rest/openehr/v1/ehr/{ehrId}/composition", content);
response.EnsureSuccessStatusCode();