Skip to content

Commit f576cae

Browse files
committed
Silently accept an unknown designated type.
When the value of a type designator slot does not correspond to any known class, instead of erroring out with a "unknown type" error, simply fallback to converting the raw object to the type we initially expected, and pass the type designator value as it is. This will allow an application to accept data that may contain objects that have been defined in an extension to the schema, that the application is not aware of. In that case, any slot that is unknown in the originally expected type will be treated as an extension slot, and stored in the extension holders slot (assuming the originally expected type does have an extension holder slot). When serialising, if the type designator slot has a value, we do _not_ override it with the actual runtime type of the object -- we simply pass it along. This way, an application that was given an unknown object can still write it back unmodified.
1 parent 345276a commit f576cae

3 files changed

Lines changed: 30 additions & 13 deletions

File tree

core/src/main/java/org/incenp/linkml/core/ObjectConverter.java

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ public class ObjectConverter implements IConverter {
5757
private final static String STRING_EXPECTED = "Invalid value type, string expected";
5858
private final static String OBJECT_EXPECTED = "Invalid value type, '%s' expected";
5959
private final static String NO_IDENTIFIER = "Missing identifier for type '%s'";
60-
private final static String UNKNOWN_TYPE = "Unknown designated type '%s'";
6160
private final static String INVALID_CLASS_URI = "Missing or invalid class URI for type '%s'";
6261

6362
protected ClassInfo klass;
@@ -188,13 +187,15 @@ public Object convert(Map<String, Object> raw, ConverterContext ctx) throws Link
188187
try {
189188
designatedType = Class.forName(pkgName + "." + designatedName);
190189
} catch ( ClassNotFoundException e ) {
191-
// We treat that as a value error because providing a correct name in the class
192-
// designator is the responsibility of whoever produced the data
193-
throw new LinkMLValueError(String.format(UNKNOWN_TYPE, designatedName));
190+
// Do nothing, simply fallback to the type we were originally expecting. This is
191+
// so an application has a chance to process data that would use an unknown
192+
// subclass (possibly defined by an extension).
194193
}
195194
}
196195

197-
conv = (ObjectConverter) ctx.getConverter(designatedType);
196+
if ( designatedType != null ) {
197+
conv = (ObjectConverter) ctx.getConverter(designatedType);
198+
}
198199
}
199200
}
200201
String id = null;
@@ -350,9 +351,9 @@ public Map<String, Object> serialise(Object object, boolean withIdentifier, Conv
350351
continue;
351352
}
352353

353-
Object slotValue;
354-
if ( slot.isTypeDesignator() ) {
355-
// Ignore whatever may be contained in the slot and use the actual type name
354+
Object slotValue = slot.getValue(object);
355+
if ( slotValue == null && slot.isTypeDesignator() ) {
356+
// Set the slot to the actual type name
356357
if ( slot.getInnerType().equals(URI.class) ) {
357358
// URI-typed designator slot, it requires the class URI
358359
try {
@@ -370,11 +371,9 @@ public Map<String, Object> serialise(Object object, boolean withIdentifier, Conv
370371
// Assume a name-based type designator slot
371372
slotValue = getType().getSimpleName();
372373
}
373-
} else {
374-
slotValue = slot.getValue(object);
375-
if ( slotValue == null ) {
376-
continue;
377-
}
374+
} else if ( slotValue == null ) {
375+
// Ignore all other empty slots
376+
continue;
378377
}
379378

380379
if ( slot.isExtensionStore() ) {

core/src/test/java/org/incenp/linkml/core/ObjectConverterTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,19 @@ void testCurieTypeDesignator() throws IOException, LinkMLRuntimeException {
411411
Assertions.assertEquals("EX:DerivedCurieSelfDesignatedClass", raw.get("type"));
412412
}
413413

414+
@Test
415+
void testUnknownTypeDesignator() throws IOException, LinkMLRuntimeException {
416+
String text = "foo: A string\nbaz: Another string\ntype: UnknownDerivedSelfDesignatedClass\n";
417+
DerivedSelfDesignatedClass dsdc = parseString(text, DerivedSelfDesignatedClass.class);
418+
419+
Assertions.assertEquals("A string", dsdc.getFoo());
420+
Assertions.assertEquals("UnknownDerivedSelfDesignatedClass", dsdc.getType());
421+
Assertions.assertNotNull(dsdc.getExtraSlots());
422+
Assertions.assertEquals("Another string", dsdc.getExtraSlots().get("baz"));
423+
424+
roundtrip(dsdc);
425+
}
426+
414427
@Test
415428
void testReferenceToIRIIdentifiers() throws IOException, LinkMLRuntimeException {
416429
ctx.addPrefix("PFX", "https://example.org/");

core/src/test/lombok/org/incenp/linkml/core/sample/BaseSelfDesignatedClass.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434

3535
package org.incenp.linkml.core.sample;
3636

37+
import java.util.Map;
38+
39+
import org.incenp.linkml.core.annotations.ExtensionHolder;
3740
import org.incenp.linkml.core.annotations.TypeDesignator;
3841

3942
import lombok.AccessLevel;
@@ -53,4 +56,6 @@ public class BaseSelfDesignatedClass {
5356
private String foo;
5457
@TypeDesignator
5558
private String type;
59+
@ExtensionHolder
60+
private Map<String, Object> extraSlots;
5661
}

0 commit comments

Comments
 (0)