diff --git a/core/src/main/java/org/incenp/linkml/core/BinaryBlobConverter.java b/core/src/main/java/org/incenp/linkml/core/BinaryBlobConverter.java
new file mode 100644
index 0000000..292027d
--- /dev/null
+++ b/core/src/main/java/org/incenp/linkml/core/BinaryBlobConverter.java
@@ -0,0 +1,75 @@
+/*
+ * LinkML-Java - LinkML library for Java
+ * Copyright © 2026 Damien Goutte-Gattat
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * (1) Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * (2) Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * (3) Neither the name of the copyright holder nor the names its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+ * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.incenp.linkml.core;
+
+import java.util.Base64;
+
+/**
+ * A converter for slots typed as xsd:base64Binary (represented as
+ * an array of byte).
+ *
+ * Of note, such a type currently does not exist in LinkML, though it can easily
+ * be added in a custom type declaration. The default Javagen templates won’t
+ * render it as a byte[], though.
+ */
+public class BinaryBlobConverter extends ScalarConverterBase {
+
+ // Not sure of a better way to get the Class> representing a byte[] array than
+ // having a dummy on which we can call getClass()... The correct way would be
+ // Byte.TYPE.arrayType(), but that method is only available from Java 12+.
+ private static byte[] array = new byte[1];
+
+ @Override
+ public Class> getType() {
+ return array.getClass();
+ }
+
+ @Override
+ protected Object convertImpl(Object raw, ConverterContext ctx) throws LinkMLRuntimeException {
+ try {
+ return Base64.getDecoder().decode(raw.toString());
+ } catch ( IllegalArgumentException e ) {
+ throw new LinkMLValueError("Invalid value, Base64-encoded binary blob expected", e);
+ }
+ }
+
+ public Object serialise(Object object, ConverterContext ctx) throws LinkMLRuntimeException {
+ if ( getType().isInstance(object) ) {
+ return Base64.getEncoder().encodeToString((byte[]) object);
+ } else {
+ throw new LinkMLInternalError("Invalid value, array of bytes expected");
+ }
+ }
+}
diff --git a/core/src/main/java/org/incenp/linkml/core/ConverterContext.java b/core/src/main/java/org/incenp/linkml/core/ConverterContext.java
index 1ba40b7..3c3e9b1 100644
--- a/core/src/main/java/org/incenp/linkml/core/ConverterContext.java
+++ b/core/src/main/java/org/incenp/linkml/core/ConverterContext.java
@@ -168,6 +168,8 @@ public ConverterContext() {
// whose range is set to the linkml:Any class).
converters.put(Object.class, new TransparentConverter());
+ addConverter(new BinaryBlobConverter());
+
objectConverterProvider = (t) -> new ObjectConverter(t);
typeResolver = new DefaultTypeDesignatorResolver();
}
diff --git a/core/src/test/java/org/incenp/linkml/core/ObjectConverterTest.java b/core/src/test/java/org/incenp/linkml/core/ObjectConverterTest.java
index 147784d..902613b 100644
--- a/core/src/test/java/org/incenp/linkml/core/ObjectConverterTest.java
+++ b/core/src/test/java/org/incenp/linkml/core/ObjectConverterTest.java
@@ -52,6 +52,7 @@
import org.incenp.linkml.core.samples.base.BaseURISelfDesignatedClass;
import org.incenp.linkml.core.samples.base.ClassWithCustomConverter;
import org.incenp.linkml.core.samples.base.ContainerOfAny;
+import org.incenp.linkml.core.samples.base.ContainerOfBinaryData;
import org.incenp.linkml.core.samples.base.ContainerOfBooleanValues;
import org.incenp.linkml.core.samples.base.ContainerOfIRIIdentifiableObjects;
import org.incenp.linkml.core.samples.base.ContainerOfIdentifiedSelfDesignatedObjects;
@@ -773,6 +774,30 @@ void testParsingWithRefinedInheritedSlots() throws IOException {
Assertions.assertEquals(2, tdf.getBars().get(0).getLength());
}
+ @Test
+ void testParsingBinaryBlobs() throws IOException {
+ ContainerOfBinaryData cobd = parseString("checksum: aGVsbG8=\nchecksums:\n - d29ybGQ=",
+ ContainerOfBinaryData.class);
+ Assertions.assertEquals("hello", new String(cobd.getChecksum()));
+ Assertions.assertEquals("world", new String(cobd.getChecksums().get(0)));
+
+ // Can't use roundtrip for now because the generated Java code for binary blobs
+ // does not handle arrays the way we'd need it (two different arrays with the
+ // same contents are not equals). The Javagen template will need to be updated
+ // to
+ // use Arrays.hashCode() and Arrays.equals for those slots.
+ try {
+ Object raw = ctx.getConverter(cobd.getClass()).serialise(cobd, ctx);
+ Object cooked = ctx.getConverter(ContainerOfBinaryData.class).convert(raw, ctx);
+ Assertions.assertInstanceOf(ContainerOfBinaryData.class, cooked);
+ Assertions.assertEquals("hello", new String(((ContainerOfBinaryData) cooked).getChecksum()));
+ Assertions.assertEquals("world", new String(((ContainerOfBinaryData) cooked).getChecksums().get(0)));
+ } catch ( LinkMLRuntimeException e ) {
+ Assertions.fail("Unexpected exception", e);
+ }
+
+ }
+
private