Summary
Currently, the annotation-based binding classes (DefinitionAssembly) don't support IChoiceInstance - the Metaschema concept of mutually exclusive field alternatives (e.g., "either field A or field B"). This limits required field validation for dynamically compiled modules.
Background
Metaschema has two related but distinct concepts:
| Concept |
Interface |
Purpose |
Current Support |
| Choice |
IChoiceInstance |
Mutually exclusive alternatives |
❌ Not supported |
| Choice Group |
IChoiceGroupInstance |
Polymorphic collection with discriminator |
✅ Supported via @BoundChoiceGroup |
The @BoundChoice annotation exists for choice groups (polymorphic collections), but there's no equivalent for choices (mutual exclusion).
Impact
When required field validation is enabled (DESERIALIZE_VALIDATE_REQUIRED_FIELDS), the validator needs to know which fields are in a choice group to avoid false positives. Currently:
- For
DefinitionAssemblyGlobal (Metaschema-loaded modules): Works correctly via getChoiceInstances()
- For
DefinitionAssembly (annotation-based bindings): Returns empty list, causing false validation errors
This affects dynamically compiled modules where the code generator creates annotation-based bindings that lose choice information.
Workarounds to Remove
When this issue is implemented, the following workarounds should be removed:
| File |
Workaround |
Description |
BindingModuleLoader.java |
boundLoader.disableFeature(DESERIALIZE_VALIDATE_REQUIRED_FIELDS) |
Disables validation for module loading |
MetaschemaModuleMetaschemaTest.java |
deserializeWithValidationDisabled() helper |
Disables validation in tests |
JsonParserTest.java |
loader.disableFeature(DESERIALIZE_VALIDATE_REQUIRED_FIELDS) |
Disables validation in choice regression test |
AbstractConvertSubcommand.java |
loader.disableFeature(DESERIALIZE_VALIDATE_REQUIRED_FIELDS) |
Disables validation in CLI convert command |
AbstractValidateContentCommand.java |
loader.disableFeature(DESERIALIZE_VALIDATE_REQUIRED_FIELDS) |
Disables validation in CLI validate command |
EvaluateMetapathCommand.java |
loader.disableFeature(DESERIALIZE_VALIDATE_REQUIRED_FIELDS) |
Disables validation in CLI metapath command |
These workarounds exist because pre-generated and dynamically compiled binding classes don't preserve choice group information.
Proposed Solution
1. New Annotation: @BoundChoice
@Retention(RUNTIME)
@Target({ FIELD })
public @interface BoundChoice {
/**
* Identifies which choice this field belongs to.
* Fields with the same choiceId are mutually exclusive.
*/
String choiceId();
}
2. New Class: InstanceModelChoice
Implement IChoiceInstance for annotation-based bindings:
public class InstanceModelChoice implements IChoiceInstance {
private final IBoundDefinitionModelAssembly parent;
private final List<IBoundInstanceModelNamed<?>> instances;
@Override
public Collection<? extends INamedModelInstanceAbsolute> getNamedModelInstances() {
return instances;
}
// ... other IChoiceInstance methods
}
3. Update AssemblyModelGenerator
Group fields by @BoundChoice annotation and create IChoiceInstance objects:
// Group fields by @BoundChoice choiceId
Map<String, List<IBoundInstanceModelNamed<?>>> choiceGroups = new LinkedHashMap<>();
for (IBoundInstanceModel<?> instance : modelInstances) {
if (instance instanceof IBoundInstanceModelNamed) {
BoundChoice annotation = ((IBoundInstanceModelNamed<?>) instance)
.getField().getAnnotation(BoundChoice.class);
if (annotation != null) {
choiceGroups.computeIfAbsent(annotation.choiceId(), k -> new ArrayList<>())
.add((IBoundInstanceModelNamed<?>) instance);
}
}
}
// Create IChoiceInstance for each group
for (List<IBoundInstanceModelNamed<?>> group : choiceGroups.values()) {
builder.append(new InstanceModelChoice(containingDefinition, group));
}
4. Update Code Generator
Emit @BoundChoice(choiceId = "choice-N") annotations on fields within Metaschema <choice> elements.
5. Adjacency Validation (Important)
Choice fields must be adjacent in the model. In Metaschema, a <choice> element defines alternatives that occupy the same position in the serialization order. Therefore:
- All fields with the same
choiceId must appear consecutively in the class
- Non-adjacent choice fields indicate a malformed binding class
- Validation should occur at binding initialization time
// During AssemblyModelGenerator initialization
private void validateChoiceAdjacency(Map<String, List<IBoundInstanceModelNamed<?>>> choiceGroups) {
for (Map.Entry<String, List<IBoundInstanceModelNamed<?>>> entry : choiceGroups.entrySet()) {
String choiceId = entry.getKey();
List<IBoundInstanceModelNamed<?>> instances = entry.getValue();
// Verify all instances have consecutive indices in the model
if (instances.size() > 1) {
List<Integer> indices = instances.stream()
.map(i -> getModelInstanceIndex(i))
.sorted()
.collect(Collectors.toList());
for (int i = 1; i < indices.size(); i++) {
if (indices.get(i) != indices.get(i-1) + 1) {
throw new IllegalStateException(
"Choice fields with choiceId '" + choiceId + "' are not adjacent");
}
}
}
}
}
This catches:
- Bugs in the code generator that don't maintain field order
- Manual editing that breaks the choice structure
- Inheritance issues where fields get interleaved
Files to Modify
| File |
Changes |
databind/.../annotations/BoundChoice.java |
New - annotation definition |
databind/.../model/impl/InstanceModelChoice.java |
New - IChoiceInstance implementation |
databind/.../model/impl/AssemblyModelGenerator.java |
Handle @BoundChoice annotation, validate adjacency |
databind/.../codegen/typeinfo/AbstractModelInstanceTypeInfo.java |
Emit @BoundChoice during code generation |
| Bootstrap binding classes |
Regenerate with new annotations |
Existing Infrastructure
The model builder already supports choice instances:
DefaultAssemblyModelBuilder.append(CI instance) - accepts choice instances
DefaultAssemblyModelBuilder.getChoiceInstances() - retrieves them
DefaultAssemblyModelBuilder.buildAssembly() - includes them in final model
Acceptance Criteria
Related
Summary
Currently, the annotation-based binding classes (
DefinitionAssembly) don't supportIChoiceInstance- the Metaschema concept of mutually exclusive field alternatives (e.g., "either field A or field B"). This limits required field validation for dynamically compiled modules.Background
Metaschema has two related but distinct concepts:
IChoiceInstanceIChoiceGroupInstance@BoundChoiceGroupThe
@BoundChoiceannotation exists for choice groups (polymorphic collections), but there's no equivalent for choices (mutual exclusion).Impact
When required field validation is enabled (
DESERIALIZE_VALIDATE_REQUIRED_FIELDS), the validator needs to know which fields are in a choice group to avoid false positives. Currently:DefinitionAssemblyGlobal(Metaschema-loaded modules): Works correctly viagetChoiceInstances()DefinitionAssembly(annotation-based bindings): Returns empty list, causing false validation errorsThis affects dynamically compiled modules where the code generator creates annotation-based bindings that lose choice information.
Workarounds to Remove
When this issue is implemented, the following workarounds should be removed:
BindingModuleLoader.javaboundLoader.disableFeature(DESERIALIZE_VALIDATE_REQUIRED_FIELDS)MetaschemaModuleMetaschemaTest.javadeserializeWithValidationDisabled()helperJsonParserTest.javaloader.disableFeature(DESERIALIZE_VALIDATE_REQUIRED_FIELDS)AbstractConvertSubcommand.javaloader.disableFeature(DESERIALIZE_VALIDATE_REQUIRED_FIELDS)AbstractValidateContentCommand.javaloader.disableFeature(DESERIALIZE_VALIDATE_REQUIRED_FIELDS)EvaluateMetapathCommand.javaloader.disableFeature(DESERIALIZE_VALIDATE_REQUIRED_FIELDS)These workarounds exist because pre-generated and dynamically compiled binding classes don't preserve choice group information.
Proposed Solution
1. New Annotation:
@BoundChoice2. New Class:
InstanceModelChoiceImplement
IChoiceInstancefor annotation-based bindings:3. Update
AssemblyModelGeneratorGroup fields by
@BoundChoiceannotation and createIChoiceInstanceobjects:4. Update Code Generator
Emit
@BoundChoice(choiceId = "choice-N")annotations on fields within Metaschema<choice>elements.5. Adjacency Validation (Important)
Choice fields must be adjacent in the model. In Metaschema, a
<choice>element defines alternatives that occupy the same position in the serialization order. Therefore:choiceIdmust appear consecutively in the classThis catches:
Files to Modify
databind/.../annotations/BoundChoice.javadatabind/.../model/impl/InstanceModelChoice.javaIChoiceInstanceimplementationdatabind/.../model/impl/AssemblyModelGenerator.java@BoundChoiceannotation, validate adjacencydatabind/.../codegen/typeinfo/AbstractModelInstanceTypeInfo.java@BoundChoiceduring code generationExisting Infrastructure
The model builder already supports choice instances:
DefaultAssemblyModelBuilder.append(CI instance)- accepts choice instancesDefaultAssemblyModelBuilder.getChoiceInstances()- retrieves themDefaultAssemblyModelBuilder.buildAssembly()- includes them in final modelAcceptance Criteria
@BoundChoiceannotation createdInstanceModelChoiceimplementsIChoiceInstanceAssemblyModelGeneratorgroups fields by choice and creates instances@BoundChoicefor fields in Metaschema choicesDefinitionAssembly.getChoiceInstances()returns proper choice instancesRelated
@BoundChoiceGroupannotation - existing support for choice groups (different concept)