diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterManager.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterManager.java
new file mode 100644
index 00000000000..09b24657b66
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterManager.java
@@ -0,0 +1,43 @@
+package ca.uhn.fhir.util.adapters;
+
+import jakarta.annotation.Nonnull;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+
+public class AdapterManager implements IAdapterManager {
+ public static final AdapterManager INSTANCE = new AdapterManager();
+
+ Set myAdapterFactories = new HashSet<>();
+
+ /**
+ * Hidden to force shared use of the public INSTANCE.
+ */
+ AdapterManager() {}
+
+ public @Nonnull Optional getAdapter(Object theObject, Class theTargetType) {
+ // todo this can be sped up with a cache of type->Factory.
+ return myAdapterFactories.stream()
+ .filter(nextFactory -> nextFactory.getAdapters().stream().anyMatch(theTargetType::isAssignableFrom))
+ .flatMap(nextFactory -> {
+ var adapter = nextFactory.getAdapter(theObject, theTargetType);
+ // can't use Optional.stream() because of our Android target is API level 26/JDK 8.
+ if (adapter.isPresent()) {
+ return Stream.of(adapter.get());
+ } else {
+ return Stream.empty();
+ }
+ })
+ .findFirst();
+ }
+
+ public void registerFactory(@Nonnull IAdapterFactory theFactory) {
+ myAdapterFactories.add(theFactory);
+ }
+
+ public void unregisterFactory(@Nonnull IAdapterFactory theFactory) {
+ myAdapterFactories.remove(theFactory);
+ }
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterUtils.java
new file mode 100644
index 00000000000..36237becd9a
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterUtils.java
@@ -0,0 +1,29 @@
+package ca.uhn.fhir.util.adapters;
+
+import java.util.Optional;
+
+public class AdapterUtils {
+
+ /**
+ * Main entry point for adapter calls.
+ * Implements three conversions: cast to the target type, use IAdaptable if present, or lastly try the AdapterManager.INSTANCE.
+ * @param theObject the object to be adapted
+ * @param theTargetType the type of the adapter requested
+ */
+ static Optional adapt(Object theObject, Class theTargetType) {
+ if (theTargetType.isInstance(theObject)) {
+ //noinspection unchecked
+ return Optional.of((T) theObject);
+ }
+
+ if (theObject instanceof IAdaptable) {
+ IAdaptable adaptable = (IAdaptable) theObject;
+ var adapted = adaptable.getAdapter(theTargetType);
+ if (adapted.isPresent()) {
+ return adapted;
+ }
+ }
+
+ return AdapterManager.INSTANCE.getAdapter(theObject, theTargetType);
+ }
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdaptable.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdaptable.java
new file mode 100644
index 00000000000..5eb1b266b49
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdaptable.java
@@ -0,0 +1,19 @@
+package ca.uhn.fhir.util.adapters;
+
+import jakarta.annotation.Nonnull;
+
+import java.util.Optional;
+
+/**
+ * Generic version of Eclipse IAdaptable interface.
+ */
+public interface IAdaptable {
+ /**
+ * Get an adapter of requested type.
+ * @param theTargetType the desired type of the adapter
+ * @return an adapter of theTargetType if possible, or empty.
+ */
+ default @Nonnull Optional getAdapter(@Nonnull Class theTargetType) {
+ return AdapterUtils.adapt(this, theTargetType);
+ }
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterFactory.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterFactory.java
new file mode 100644
index 00000000000..a183705cc53
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterFactory.java
@@ -0,0 +1,25 @@
+package ca.uhn.fhir.util.adapters;
+
+import java.util.Collection;
+import java.util.Optional;
+
+/**
+ * Interface for external service that builds adaptors for targets.
+ */
+public interface IAdapterFactory {
+ /**
+ * Build an adaptor for the target.
+ * May return empty() even if the target type is listed in getAdapters() when
+ * the factory fails to convert a particular instance.
+ *
+ * @param theObject the object to be adapted.
+ * @param theAdapterType the target type
+ * @return the adapter, if possible.
+ */
+ Optional getAdapter(Object theObject, Class theAdapterType);
+
+ /**
+ * @return the collection of adapter target types handled by this factory.
+ */
+ Collection> getAdapters();
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterManager.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterManager.java
new file mode 100644
index 00000000000..9550ccbdc27
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterManager.java
@@ -0,0 +1,10 @@
+package ca.uhn.fhir.util.adapters;
+
+import java.util.Optional;
+
+/**
+ * Get an adaptor
+ */
+public interface IAdapterManager {
+ Optional getAdapter(Object theTarget, Class theAdapter);
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/package-info.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/package-info.java
new file mode 100644
index 00000000000..de9e92df5c4
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/package-info.java
@@ -0,0 +1,20 @@
+/**
+ * Implements the Adapter pattern to allow external classes to extend/adapt existing classes.
+ * Useful for extending interfaces that are closed to modification, or restricted for classpath reasons.
+ *
+ * For clients, the main entry point is {@link ca.uhn.fhir.util.adapters.AdapterUtils#adapt(java.lang.Object, java.lang.Class)}
+ * which will attempt to cast to the target type, or build an adapter of the target type.
+ *
+ *
+ * For implementors, you can support adaptation via two mechanisms:
+ *
+ * - by implementing {@link ca.uhn.fhir.util.adapters.IAdaptable} directly on a class to provide supported adapters,
+ *
- or when the class is closed to direct modification, you can implement
+ * an instance of {@link ca.uhn.fhir.util.adapters.IAdapterFactory} and register
+ * it with the public {@link ca.uhn.fhir.util.adapters.AdapterManager#INSTANCE}.
+ *
+ * The AdapterUtils.adapt() supports both of these.
+ *
+ * Inspired by the Eclipse runtime.
+ */
+package ca.uhn.fhir.util.adapters;
diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterManagerTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterManagerTest.java
new file mode 100644
index 00000000000..ee621533587
--- /dev/null
+++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterManagerTest.java
@@ -0,0 +1,78 @@
+package ca.uhn.fhir.util.adapters;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AdapterManagerTest {
+ AdapterManager myAdapterManager = new AdapterManager();
+
+ @AfterAll
+ static void tearDown() {
+ assertThat(AdapterManager.INSTANCE.myAdapterFactories)
+ .withFailMessage("Don't dirty the public instance").isEmpty();
+ }
+
+ @Test
+ void testRegisterFactory_providesAdapter() {
+ // given
+ myAdapterManager.registerFactory(new StringToIntFactory());
+
+ // when
+ var result = myAdapterManager.getAdapter("22", Integer.class);
+
+ // then
+ assertThat(result).contains(22);
+ }
+
+ @Test
+ void testRegisterFactory_wrongTypeStillEmpty() {
+ // given
+ myAdapterManager.registerFactory(new StringToIntFactory());
+
+ // when
+ var result = myAdapterManager.getAdapter("22", Float.class);
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ void testUnregisterFactory_providesEmpty() {
+ // given active factory, now gone.
+ StringToIntFactory factory = new StringToIntFactory();
+ myAdapterManager.registerFactory(factory);
+ myAdapterManager.getAdapter("22", Integer.class);
+ myAdapterManager.unregisterFactory(factory);
+
+ // when
+ var result = myAdapterManager.getAdapter("22", Integer.class);
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+
+ static class StringToIntFactory implements IAdapterFactory {
+ @Override
+ public Optional getAdapter(Object theObject, Class theAdapterType) {
+ if (theObject instanceof String s) {
+ if (theAdapterType.isAssignableFrom(Integer.class)) {
+ @SuppressWarnings("unchecked")
+ T i = (T) Integer.valueOf(s);
+ return Optional.of(i);
+ }
+ }
+ return Optional.empty();
+ }
+
+ public Collection> getAdapters() {
+ return List.of(Integer.class);
+ }
+ }
+}
diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterUtilsTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterUtilsTest.java
new file mode 100644
index 00000000000..c7dfef8661b
--- /dev/null
+++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterUtilsTest.java
@@ -0,0 +1,123 @@
+package ca.uhn.fhir.util.adapters;
+
+import jakarta.annotation.Nonnull;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AdapterUtilsTest {
+
+ final private IAdapterFactory myTestFactory = new TestAdaptorFactory();
+
+ @AfterEach
+ void tearDown() {
+ AdapterManager.INSTANCE.unregisterFactory(myTestFactory);
+ }
+
+ @Test
+ void testNullDoesNotAdapt() {
+
+ // when
+ var adapted = AdapterUtils.adapt(null, InterfaceA.class);
+
+ // then
+ assertThat(adapted).isEmpty();
+ }
+
+ @Test
+ void testAdaptObjectImplementingInterface() {
+ // given
+ var object = new ClassB();
+
+ // when
+ var adapted = AdapterUtils.adapt(object, InterfaceA.class);
+
+ // then
+ assertThat(adapted)
+ .isPresent()
+ .get().isInstanceOf(InterfaceA.class);
+ assertThat(adapted.get()).withFailMessage("Use object since it implements interface").isSameAs(object);
+ }
+
+ @Test
+ void testAdaptObjectImplementingAdaptorSupportingInterface() {
+ // given
+ var object = new SelfAdaptableClass();
+
+ // when
+ var adapted = AdapterUtils.adapt(object, InterfaceA.class);
+
+ // then
+ assertThat(adapted)
+ .isPresent()
+ .get().isInstanceOf(InterfaceA.class);
+ }
+
+ @Test
+ void testAdaptObjectViaAdapterManager() {
+ // given
+ var object = new ManagerAdaptableClass();
+ AdapterManager.INSTANCE.registerFactory(myTestFactory);
+
+ // when
+ var adapted = AdapterUtils.adapt(object, InterfaceA.class);
+
+ // then
+ assertThat(adapted)
+ .isPresent()
+ .get().isInstanceOf(InterfaceA.class);
+ }
+
+ interface InterfaceA {
+
+ }
+
+ static class ClassB implements InterfaceA {
+
+ }
+
+ /** class that can adapt itself to IAdaptable */
+ static class SelfAdaptableClass implements IAdaptable {
+
+ @Nonnull
+ @Override
+ public Optional getAdapter(@Nonnull Class theTargetType) {
+ if (theTargetType.isAssignableFrom(InterfaceA.class)) {
+ T value = theTargetType.cast(buildInterfaceAWrapper(this));
+ return Optional.of(value);
+ }
+ return Optional.empty();
+ }
+ }
+
+ private static @Nonnull InterfaceA buildInterfaceAWrapper(Object theObject) {
+ return new InterfaceA() {};
+ }
+
+ /** Class that relies on an external IAdapterFactory */
+ static class ManagerAdaptableClass {
+ }
+
+
+ static class TestAdaptorFactory implements IAdapterFactory {
+
+ @Override
+ public Optional getAdapter(Object theObject, Class theAdapterType) {
+ if (theObject instanceof ManagerAdaptableClass && theAdapterType == InterfaceA.class) {
+ T adapter = theAdapterType.cast(buildInterfaceAWrapper(theObject));
+ return Optional.of(adapter);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public Collection> getAdapters() {
+ return Set.of(InterfaceA.class);
+ }
+ }
+}