Skip to content

Support OID-based CodeSystem and ValueSet resolution - #8188

Open
gM4n-sys wants to merge 6 commits into
hapifhir:masterfrom
gM4n-sys:issue-8176
Open

Support OID-based CodeSystem and ValueSet resolution#8188
gM4n-sys wants to merge 6 commits into
hapifhir:masterfrom
gM4n-sys:issue-8176

Conversation

@gM4n-sys

@gM4n-sys gM4n-sys commented Jul 23, 2026

Copy link
Copy Markdown

Fixes #8176

Summary

This PR adds identifier-based fallback resolution for canonical terminology resources referenced by OID URNs.

It supports ValueSet expansion when:

  • ValueSet.compose.include.system references a CodeSystem by OID
  • ValueSet.compose.include.valueSet references another ValueSet by OID

The installed CodeSystem or ValueSet may continue to use its HTTP canonical URL in url while exposing the OID through:

{
  "identifier": [
    {
      "system": "urn:ietf:rfc:3986",
      "value": "urn:oid:1.2.3.4"
    }
  ]
}

Previously, HAPI FHIR attempted to resolve these references only through the resource's canonical url. An OID reference therefore failed whenever the installed resource used a different HTTP canonical URL.

Companion PR

The corresponding HAPI FHIR JPA Server Starter compatibility changes are provided by:

The companion PR updates the starter for the DaoRegistry registration flow and was used for the end-to-end runtime verification described below.

Resolution behavior

Canonical resource resolution now uses two distinct phases:

  1. Search the complete validation-support chain for an exact canonical URL match.
  2. Only if no canonical match exists and the requested reference starts with urn:oid:, search by identifier.

The identifier fallback requires:

identifier.system = urn:ietf:rfc:3986
identifier.value  = the requested urn:oid value

When the reference includes a version, the resource version must also match exactly.

For example:

urn:oid:1.2.3.4|4.0.0

matches only a resource with:

identifier.system = urn:ietf:rfc:3986
identifier.value  = urn:oid:1.2.3.4
version           = 4.0.0

Canonical URL resolution retains priority across the complete validation-support chain. An identifier match from an earlier support cannot override an exact canonical match from a later support.

Nested ValidationSupportChain instances preserve the same canonical-first ordering.

Changes

This PR:

  • Adds CanonicalResourceIdentifierRequest.

  • Adds the FHIR-version-independent CanonicalResourceIdentifierMatcher.

  • Adds IValidationSupport.fetchCanonicalResourceByIdentifier(...).

  • Generalizes identifier fallback beyond CodeSystem-specific resolution.

  • Adds canonical-first, identifier-second resolution for:

    • CodeSystem
    • ValueSet
  • Adds backend-specific identifier resolution for:

    • PrePopulatedValidationSupport
    • persisted JPA resources
    • the JPA NPM package cache
  • Uses indexed identifier and version searches for persisted JPA resources.

  • Restricts in-memory and package lookups to candidates of the requested resource type.

  • Requires exact identifier-system and identifier-value matches.

  • Requires an exact version match when a version is requested.

  • Collapses duplicate representations of the same canonical URL and version.

  • Rejects ambiguous identifier mappings to different canonical targets.

  • Preserves validation-support ordering during identifier fallback.

  • Preserves the original OID from ValueSet.compose.include.system in ValueSet.expansion.contains.system.

The default IValidationSupport implementation returns no identifier result. Each backend therefore controls how it performs an optimized lookup instead of falling back to an unrestricted scan of all conformance resources.

Supported examples

CodeSystem referenced by OID

A ValueSet may contain:

{
  "compose": {
    "include": [
      {
        "system": "urn:oid:1.3.6.1.4.1.19376.3.276.1.5.9",
        "version": "4.0.0"
      }
    ]
  }
}

while the installed CodeSystem contains:

{
  "url": "https://example.org/CodeSystem/example",
  "identifier": [
    {
      "system": "urn:ietf:rfc:3986",
      "value": "urn:oid:1.3.6.1.4.1.19376.3.276.1.5.9"
    }
  ],
  "version": "4.0.0"
}

The CodeSystem is resolved by its identifier, and the expansion continues to use the requested OID as expansion.contains.system.

ValueSet imported by OID

A ValueSet may import another ValueSet using:

{
  "compose": {
    "include": [
      {
        "valueSet": [
          "urn:oid:1.2.3.4|4.0.0"
        ]
      }
    ]
  }
}

The imported ValueSet can use an HTTP canonical URL while exposing urn:oid:1.2.3.4 through ValueSet.identifier.

Verification

The change is covered at multiple levels.

Matcher tests

Tests verify:

  • CodeSystem identifier matching
  • ValueSet identifier matching
  • exact version matching
  • version mismatch behavior
  • resources without canonical URLs
  • duplicate canonical-target collapsing
  • ambiguous identifier mappings
  • null candidate collections

Validation-support chain tests

Tests verify:

  • CodeSystem OID fallback
  • ValueSet OID fallback
  • canonical URL priority over earlier identifier matches
  • nested-chain canonical priority
  • first matching validation support wins during the identifier phase
  • later validation supports are not queried after an identifier match

Backend tests

Tests cover:

  • persisted JPA CodeSystem lookup
  • persisted JPA ValueSet lookup
  • indexed identifier and version search parameters
  • NPM-JPA CodeSystem lookup
  • NPM-JPA ValueSet lookup
  • NPM-JPA StructureDefinition lookup capability
  • unsupported resource types
  • version mismatch behavior

Expansion tests

End-to-end R4 expansion tests verify:

  1. A CodeSystem referenced through ValueSet.compose.include.system is resolved by its OID identifier.
  2. A ValueSet referenced through ValueSet.compose.include.valueSet is resolved by its OID identifier and imported successfully.

The relevant validation, JPA, NPM-JPA and expansion tests pass together in the Maven reactor.

Runtime verification

The change was also verified using a complete HAPI FHIR JPA Server Starter build from hapifhir/hapi-fhir-jpaserver-starter#985 against this PR.

The resulting Java 21 server was run against PostgreSQL with an existing R4 terminology repository.

The following checks completed successfully:

  • The Spring application context initialized successfully.
  • The FHIR endpoint and normal resource searches were available.
  • ValueSet/$expand resolved CodeSystems referenced through OID identifiers.
  • ValueSet/$expand resolved imported ValueSets referenced through OID identifiers.
  • Versioned OID references were resolved correctly.
  • The requested OID was preserved in ValueSet.expansion.contains.system.
  • The Gematik ePA XDS event-code ValueSet expanded successfully.
  • The complete paginated expansion contained all expected concepts from:
    • canonical CodeSystem references
    • CodeSystem OID references
    • canonical ValueSet imports
    • ValueSet OID imports

The tested runtime combination was:

hapifhir/hapi-fhir#8188
hapifhir/hapi-fhir-jpaserver-starter#985
Dockerfile
# syntax=docker/dockerfile:1.7

ARG MAVEN_IMAGE=docker.io/library/maven:3.9.12-eclipse-temurin-17
ARG JAVA_RUNTIME_IMAGE=gcr.io/distroless/java21-debian13:nonroot

# Build the exact head tree of hapifhir/hapi-fhir PR #8188.
ARG HAPI_FHIR_REPOSITORY=https://github.com/hapifhir/hapi-fhir.git
ARG HAPI_FHIR_REF=refs/pull/8188/head

# Build the exact head tree of hapifhir/hapi-fhir-jpaserver-starter PR #985.
ARG HAPI_STARTER_REPOSITORY=https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git
ARG HAPI_STARTER_REF=refs/pull/985/head

ARG OPENTELEMETRY_JAVA_AGENT_VERSION=2.24.0


FROM ${MAVEN_IMAGE} AS build

ARG HAPI_FHIR_REPOSITORY
ARG HAPI_FHIR_REF

ARG HAPI_STARTER_REPOSITORY
ARG HAPI_STARTER_REF

ARG OPENTELEMETRY_JAVA_AGENT_VERSION

ENV MAVEN_OPTS="-Djdk.lang.Process.launchMechanism=vfork"

WORKDIR /build


#
# HAPI FHIR PR #8188
#

RUN set -eux; \
    git init hapi-fhir; \
    cd hapi-fhir; \
    git remote add origin "${HAPI_FHIR_REPOSITORY}"; \
    git fetch --depth 1 origin "${HAPI_FHIR_REF}"; \
    git checkout --detach FETCH_HEAD; \
    ACTUAL_SHA="$(git rev-parse HEAD)"; \
    printf 'HAPI FHIR PR head: %s\n' "${ACTUAL_SHA}"; 

WORKDIR /build/hapi-fhir

RUN --mount=type=cache,id=hapi-pr-8188-985-maven,target=/root/.m2 \
    set -eux; \
    HAPI_VERSION="$( \
        mvn -q \
            -DforceStdout \
            help:evaluate \
            -Dexpression=project.version \
        | tail -n 1 \
    )"; \
    test -n "${HAPI_VERSION}"; \
    printf '%s\n' "${HAPI_VERSION}" > /build/hapi-version; \
    printf 'HAPI FHIR version: %s\n' "${HAPI_VERSION}"; \
    mvn -ntp install \
        -Dmaven.test.skip=true \
        -Dmaven.javadoc.skip=true \
        -Dmaven.source.skip=true \
        -Djdk.lang.Process.launchMechanism=vfork


#
# JPA Server Starter PR #985
#

WORKDIR /build

RUN set -eux; \
    git init hapi-fhir-jpaserver-starter; \
    cd hapi-fhir-jpaserver-starter; \
    git remote add origin "${HAPI_STARTER_REPOSITORY}"; \
    git fetch --depth 1 origin "${HAPI_STARTER_REF}"; \
    git checkout --detach FETCH_HEAD; \
    ACTUAL_SHA="$(git rev-parse HEAD)"; \
    printf 'Starter PR head: %s\n' "${ACTUAL_SHA}"; 

WORKDIR /build/hapi-fhir-jpaserver-starter

ENV MAVEN_OPTS="-Xms512m -Xmx4g -XX:+UseG1GC -Djdk.lang.Process.launchMechanism=vfork"

RUN --mount=type=cache,id=hapi-pr-8188-985-maven,target=/root/.m2 \
    set -eux; \
    HAPI_VERSION="$(cat /build/hapi-version)"; \
    STARTER_HAPI_VERSION="$( \
        mvn -q \
            -DforceStdout \
            help:evaluate \
            -Dexpression=project.parent.version \
        | tail -n 1 \
    )"; \
    printf 'HAPI FHIR version:      %s\n' "${HAPI_VERSION}"; \
    printf 'Starter parent version: %s\n' "${STARTER_HAPI_VERSION}"; \
    test "${STARTER_HAPI_VERSION}" = "${HAPI_VERSION}"; \
    mvn -ntp spotless:check; \
    mvn -ntp clean package \
        spring-boot:repackage \
        -Pboot \
        -Dmaven.test.skip=true \
        -Dmaven.javadoc.skip=true \
        -Dmaven.source.skip=true \
        -Djdk.lang.Process.launchMechanism=vfork; \
    test -s target/ROOT.war


#
# Prepare runtime files using the same layout as the official starter image.
#

RUN set -eux; \
    mkdir -p /build/runtime; \
    cp target/ROOT.war /build/runtime/main.war; \
    cp src/main/java/HealthCheck.java /build/runtime/HealthCheck.java; \
    javac -d /build/runtime /build/runtime/HealthCheck.java; \
    rm /build/runtime/HealthCheck.java; \
    curl -fL \
        --retry 3 \
        --output /build/runtime/opentelemetry-javaagent.jar \
        "https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v${OPENTELEMETRY_JAVA_AGENT_VERSION}/opentelemetry-javaagent.jar"; \
    test -s /build/runtime/main.war; \
    test -s /build/runtime/HealthCheck.class; \
    test -s /build/runtime/opentelemetry-javaagent.jar


#
# Runtime
#

FROM ${JAVA_RUNTIME_IMAGE}

LABEL org.opencontainers.image.title="HAPI FHIR PR #8188 with Starter PR #985"
LABEL org.opencontainers.image.source="https://github.com/hapifhir/hapi-fhir/pull/8188"
LABEL org.hapifhir.starter.source="https://github.com/hapifhir/hapi-fhir-jpaserver-starter/pull/985"

USER 65532:65532
WORKDIR /app

COPY --chown=65532:65532 \
    --from=build \
    /build/runtime/ \
    /app/

ENTRYPOINT ["java", "--class-path", "/app/main.war", "-Dloader.path=main.war!/WEB-INF/classes/,main.war!/WEB-INF/,/app/extra-classes", "org.springframework.boot.loader.PropertiesLauncher"]

Non-goals

This change does not alter REST search semantics such as:

GET /CodeSystem?url=...
GET /ValueSet?url=...

It also does not treat arbitrary identifiers as canonical aliases. Identifier fallback is currently restricted to OID URNs represented as:

identifier.system = urn:ietf:rfc:3986
identifier.value  = urn:oid:...

@gM4n-sys gM4n-sys closed this Jul 23, 2026
@gM4n-sys
gM4n-sys deleted the issue-8176 branch July 23, 2026 12:00
@gM4n-sys
gM4n-sys restored the issue-8176 branch July 23, 2026 12:01
@gM4n-sys gM4n-sys reopened this Jul 23, 2026
@gM4n-sys
gM4n-sys marked this pull request as draft July 23, 2026 16:34
@gM4n-sys gM4n-sys changed the title Support CodeSystem resolution by OID identifier Support OID-based CodeSystem and ValueSet resolution Jul 24, 2026
@gM4n-sys
gM4n-sys marked this pull request as ready for review July 24, 2026 16:13
GL-8718: Generalise patient SP Patient compartment membership beyond …
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ValueSet expansion fails when canonical references use OID identifiers

1 participant