Skip to content

Type Serialization

← Global Options | Next: SuperType Serialization →


See also:


1. Format × Strategy for Type

Type serialization uses two orthogonal dimensions:

  1. Format (typeFormat): PLAIN | STRUCTURED - how the data is presented
  2. Strategy (typeStrategy): URI | NAME | NUMERIC | SCHEMA_AND_TYPE | NONE - what information is transported

Both can have independent scopes (typeScope, typeFormatScope) that control where they apply. See Configuration Resolution (section 5) for scope details.

Note: MAPPED was removed from TypeStrategy. Discriminator-based type resolution is now a separate optional layer. See Discriminator Mapping for details.

1.1 PLAIN Format Examples

StrategyOutput Example
URI"_type": "http://example.org/person/1.0#//Person"
NAME"_type": "Person"
CLASS"_type": "org.example.Person"
NUMERIC"_type": "3"
SCHEMA_AND_TYPE"_schema": "http://example.org/person/1.0", "_type": "Person"
NONE(no type field written)

Note: CLASS strategy requires EClass.getInstanceClassName() to be set (typical for EMF-generated code). If the instance class name is null, serialization fails with an ERROR. See section 6.4.5 for deserialization details and recommendations.

1.2 STRUCTURED Format Examples

StrategyOutput Example
URI"_type": { "type": "http://example.org/person/1.0#//Person" }
NAME"_type": { "type": "Person" }
CLASS"_type": { "type": "org.example.Person" }
NUMERIC"_type": { "schema": "http://example.org/person/1.0", "classifier": 3 }
SCHEMA_AND_TYPE"_type": { "schema": "http://example.org/person/1.0", "type": "Person" }
NONE(no type field written)

1.3 Discriminator Mapping (Separate Layer)

Discriminator-based type resolution is not a TypeStrategy - it's an orthogonal layer that works alongside any strategy. This enables type discrimination based on a value found within the data itself.

Full Documentation: See Discriminator Mapping for complete details including:

  • Type Mapping Registry (centralized and distributed configuration)
  • Inline Mapping on EReference
  • Fallback strategy and error handling
  • Programmatic configuration

Use cases:

  • IoT/LoRaWAN devices - where device type is embedded in payload metadata
  • JSON Schema oneOf patterns - where type is determined by feature presence or values
  • Legacy APIs - where type information is encoded in application-specific fields

Resolution priority during deserialization:

  1. Type Mapping Registry (if configured on EClass) - highest priority
  2. Inline Mapping (if configured on EReference)
  3. Type Strategy Resolution (URI, NAME, etc.)
  4. Fallback (reference type, CODEC_FEATURE_TYPE_HINTS, CODEC_ROOT_TYPE)

1.4 NONE Strategy

The NONE strategy indicates that no type information should be written or expected.

Serialization with NONE:

  • No type field (_type) is written
  • Useful for homogeneous collections, APIs that don't expect type metadata, or compact output

Deserialization with NONE:

  • Deserializer does NOT look for type field in JSON
  • Type resolution relies entirely on:
    1. CODEC_ROOT_TYPE load option (for root object)
    2. EReference.getEReferenceType() (for nested objects)
  • ERROR if reference type is abstract and no concrete type can be determined

Example:

java
CodecConfiguration config = CodecConfiguration.builder()
    .typeStrategy(TypeStrategy.NONE)
    .build();

// Serialization output - no _type field
// { "name": "John", "age": 30 }

// Deserialization - MUST provide CODEC_ROOT_TYPE
Map<String, Object> options = Map.of(
    CodecResource.CODEC_ROOT_TYPE, PersonPackage.Literals.PERSON
);
resource.load(input, options);

1.6 SCHEMA_AND_TYPE Strategy

The SCHEMA_AND_TYPE strategy transports both schema URI and type name as separate pieces of information.

PLAIN format - two separate top-level fields:

json
{
  "_schema": "http://example.org/person/1.0",
  "_type": "Person"
}

STRUCTURED format - nested object with both:

json
{
  "_type": {
    "schema": "http://example.org/person/1.0",
    "type": "Person"
  }
}

Configurable keys (PLAIN):

  • typeSchemaKey: schema field (default: _schema)
  • typeKey: type field (default: _type)

Configurable keys (STRUCTURED):

  • typeKey: outer container key (default: _type)
  • typeSchemaKey: schema field inside object (default: schema)
  • typeNameKey: type name field inside object (default: type)
  • superTypeKey: supertype field inside object (default: supertype, optional)

1.7 Type Strategy Scope and Containment Behavior

Type strategy configuration applies per-class, not globally to all objects in a hierarchy. This has important implications for containment references.

1.7.1 Strategy Scope Rules

StrategyApplies ToChildren Behavior
URIConfigured classChildren use their own configured strategy (or default URI)
SCHEMA_AND_TYPERoot object onlyChildren fall back to default (URI)
NAMEConfigured classChildren use their own configured strategy (or default URI)
CLASSConfigured classChildren use their own configured strategy
NUMERICConfigured classChildren use their own configured strategy
NONEConfigured classChildren use their own configured strategy

Key Rule: SCHEMA_AND_TYPE is a root-only strategy. It establishes a context schema at the root level but does not propagate to contained objects.

1.7.2 Example: SCHEMA_AND_TYPE with Containments

Model:

  • Company (configured with SCHEMA_AND_TYPE)
  • Person (no configuration → uses default URI)
  • Company.employees: Person[*] (containment)

Configuration on Company:

xml
<eClassifiers xsi:type="ecore:EClass" name="Company">
  <eAnnotations source="http://eclipse.org/fennec/codec">
    <details key="typeStrategy" value="SCHEMA_AND_TYPE"/>
  </eAnnotations>
  ...
</eClassifiers>

Output:

json
{
  "_schema": "http://example.org/1.0",
  "_type": "Company",
  "name": "Acme Inc",
  "employees": [
    {
      "_type": "http://example.org/1.0#//Person",
      "name": "Alice"
    },
    {
      "_type": "http://example.org/1.0#//Person",
      "name": "Bob"
    }
  ]
}

Observations:

  • Root Company: uses _schema + simple _type (SCHEMA_AND_TYPE strategy)
  • Contained Person: uses full URI (default strategy, not inherited from parent)

1.5.3 Combining with Smart Compression

To achieve compact output where contained objects use simple type names, enable Smart Compression (see Global Options - Smart Compression).

With Smart Compression enabled:

json
{
  "_schema": "http://example.org/1.0",
  "_type": "Company",
  "name": "Acme Inc",
  "employees": [
    {
      "_type": "Person",
      "name": "Alice"
    }
  ]
}

Smart Compression uses the root's schema context to simplify same-schema types to simple names.

1.5.4 Per-Reference Type Configuration

Type configuration is supported at EReference level, allowing different type handling for specific references:

java
CodecConfiguration config = CodecConfiguration.builder()
    .typeStrategy(TypeStrategy.URI)  // Global default
    .forReference(CompanyPackage.Literals.COMPANY__EXTERNAL_PARTNER)
        .typeStrategy(TypeStrategy.NAME)
        .typeKey("@type")
    .build();

Output:

json
{
  "_type": "http://example.org#//Company",
  "employees": [
    { "_type": "http://example.org#//Person" }
  ],
  "externalPartner": {
    "@type": "Partner"
  }
}

Use cases:

  • External APIs expecting different type conventions
  • Cross-package references needing full URIs while internal refs use simple names
  • JSON-LD style output for specific references

Note: Per-reference configuration overrides EClass configuration, which overrides global configuration. See Configuration Resolution (section 4).


1.7 NUMERIC Strategy

The NUMERIC strategy uses EMF classifier IDs instead of type names for compactness.

PLAIN format - just the classifier ID as string:

json
{
  "_type": "3"
}

STRUCTURED format - schema + classifier ID:

json
{
  "_type": { "schema": "http://example.org/person/1.0", "classifier": 3 }
}

Configurable keys (STRUCTURED):

  • typeKey: outer key (default: _type)
  • typeSchemaKey: schema field (default: schema)
  • classifierKey: classifier ID field (default: classifier)

Warning: Classifier IDs are positional and can change when the model evolves. See Global Options - NUMERIC Strategy for details.

Deserialization requirement: NUMERIC strategy requires a schema hint (CODEC_ROOT_SCHEMA or CODEC_ROOT_TYPE) for deserialization. Classifier IDs are package-specific — without knowing which package, the ID cannot be resolved to an EClass. Without a hint, resolution fails with a WARNING diagnostic (S-4: no global EPackage scan).


2. SuperType in STRUCTURED Format

When Type format is STRUCTURED and SuperType is enabled, supertype information is included inside the _type object as a supertype field:

json
{
  "_type": {
    "schema": "http://example.org/person/1.0",
    "type": "Person",
    "supertype": ["Entity", "http://audit.org/1.0#//Auditable"]
  }
}

SuperType values follow namespace matching rules:

  • Same namespace as schema: simple EClass name (e.g., "Entity")
  • Different namespace: full EClass URI (e.g., "http://audit.org/1.0#//Auditable")

See SuperType Serialization for complete SuperType configuration including selection modes, presentation styles, and PLAIN format examples.


3. Type Configuration

The configuration defines format, strategy, and keys - not actual values. Values come from the EObject at runtime.

See also: Annotation Reference (Type Configuration) for complete key reference and scope applicability.

3.1 Configuration Keys

Annotation KeyProperty KeyDefaultDescription
typeStrategycodec.typeStrategyURIWhat type information to transport
typeFormatcodec.typeFormatPLAINHow data is presented (flat vs nested)
typeKeycodec.typeKey_typeOuter key (both formats)
typeNameKeycodec.typeNameKeytypeInner type name key (STRUCTURED only)
typeSchemaKeycodec.typeSchemaKeyschemaInner schema key (STRUCTURED only)
typeValueWriterNamecodec.typeValueWriterNameCustom value writer service name (full delegation, see §5.0 step 3a)
typeValueReaderNamecodec.typeValueReaderNameCustom value reader service name (full delegation, see §6.3.0 step 3a)
codec.typeScopeALLStrategy scope (runtime-only, see below)
codec.typeFormatScopeALLFormat scope (runtime-only, see below)

Note: typeScope and typeFormatScope are runtime-only configuration - no EAnnotation equivalent. They control where strategy/format applies (ROOT_ONLY, ROOT_CONTAINMENT, etc.). If found in EAnnotation, a WARNING is logged and the annotation is ignored. See Configuration Resolution (section 5).

Recommendation: Use keys that start with _ or @ (like _type, @type) for typeKey, typeSchemaKey, and typeNameKey. This helps distinguish metadata from data fields. Without such prefixes, there's risk of collision with ordinary data keys during deserialization - the deserializer might misinterpret a data field as type information.

3.2 EAnnotation (on EClass or EReference)

xml
<eClassifiers xsi:type="ecore:EClass" name="Person">
  <eAnnotations source="http://eclipse.org/fennec/codec">
    <details key="typeStrategy" value="SCHEMA_AND_TYPE"/>
    <details key="typeFormat" value="STRUCTURED"/>
    <details key="typeKey" value="_type"/>
  </eAnnotations>
</eClassifiers>

Type configuration can also be applied at EReference level for per-reference overrides:

xml
<eStructuralFeatures xsi:type="ecore:EReference" name="externalPartner">
  <eAnnotations source="http://eclipse.org/fennec/codec">
    <details key="typeStrategy" value="URI"/>
    <details key="typeKey" value="@type"/>
  </eAnnotations>
</eStructuralFeatures>

3.3 Java Builder (Runtime Override)

Minimal (default: PLAIN format, URI strategy):

java
CodecConfiguration config = CodecConfiguration.builder().build();

Resulting JSON:

json
{
  "_type": "http://example.org/person/1.0#//Person"
}

PLAIN format with NAME strategy:

java
CodecConfiguration config = CodecConfiguration.builder()
    .typeFormat(SerializationFormat.PLAIN)
    .typeStrategy(TypeStrategy.NAME)
    .build();

Resulting JSON:

json
{
  "_type": "Person"
}

STRUCTURED format with SCHEMA_AND_TYPE strategy:

java
CodecConfiguration config = CodecConfiguration.builder()
    .typeFormat(SerializationFormat.STRUCTURED)
    .typeStrategy(TypeStrategy.SCHEMA_AND_TYPE)
    .build();

Resulting JSON:

json
{
  "_type": {
    "schema": "http://example.org/person/1.0",
    "type": "Person"
  }
}

With custom keys:

java
CodecConfiguration config = CodecConfiguration.builder()
    .typeFormat(SerializationFormat.STRUCTURED)
    .typeStrategy(TypeStrategy.SCHEMA_AND_TYPE)
    .typeKey("@context")
    .typeSchemaKey("@vocab")
    .typeNameKey("@type")
    .build();

Resulting JSON:

json
{
  "@context": {
    "@vocab": "http://example.org/person/1.0",
    "@type": "Person"
  }
}

With scope control (runtime-only):

java
CodecConfiguration config = CodecConfiguration.builder()
    .typeStrategy(TypeStrategy.SCHEMA_AND_TYPE)
    .typeScope(StrategyScope.ROOT_ONLY)           // Strategy at root only
    .typeFormat(SerializationFormat.STRUCTURED)
    .typeFormatScope(StrategyScope.ROOT_CONTAINMENT)  // Format at root + containments
    .build();
Object LevelStrategyFormat
RootSCHEMA_AND_TYPESTRUCTURED
ContainmentURI (fallback)STRUCTURED
Non-containmentURI (fallback)PLAIN (fallback)

At serialization time:

  1. Serializer resolves effective config for current context (class, reference, scope)
  2. Determines output structure from typeFormat (PLAIN vs STRUCTURED)
  3. Determines content from typeStrategy (URI, NAME, SCHEMA_AND_TYPE, etc.)
  4. Extracts actual values from EObject's EClass (schema URI, name)
  5. Combines config (format/strategy/keys) + EObject data (values) → JSON output

4. Default Type Settings

SettingProperty KeyDefault Value
Formatcodec.typeFormatPLAIN
Strategycodec.typeStrategyURI
Type Keycodec.typeKey_type
Schema Keycodec.typeSchemaKeyschema
Name Keycodec.typeNameKeytype
Scopecodec.typeScopeALL
Format Scopecodec.typeFormatScopeALL

Note: To disable type serialization, use typeStrategy=NONE.

Default Output (PLAIN + URI):

json
{
  "_type": "http://example.org/person/1.0#//Person"
}

STRUCTURED + SCHEMA_AND_TYPE Output:

json
{
  "_type": {
    "schema": "http://example.org/person/1.0",
    "type": "Person"
  }
}

5. Serialization

The serializer writes type information based on a priority chain: discriminator mappings first, then type strategy.

5.0 Type Serialization Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│                     TYPE SERIALIZATION FLOW                                 │
└─────────────────────────────────────────────────────────────────────────────┘

INPUT: EObject to serialize, context (EReference if nested, save options)

┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. TYPE MAPPING REGISTRY (on EClass)                                        │
│    ─────────────────────────────────────────────────────────────────────────│
│    Is there a typeMapping annotation on this EClass (or its base)?          │
│                                                                             │
│    NO → Continue to step 2                                                  │
│                                                                             │
│    YES:                                                                     │
│      → Get discriminatorPath (e.g., "info.profileName")                     │
│      → Get discriminatorValue for this EClass:                              │
│          - From typeDiscriminator annotation on this EClass, OR             │
│          - Reverse lookup in registry mappings (EClass → value)             │
│      → Found? ────────────────────────────────────────────────────────────  │
│        │ YES → Write discriminatorValue at discriminatorPath                │
│        │       Do NOT write _type field ─────────────────→ DONE ✓           │
│        │                                                                    │
│        └ NO (EClass not in registry) → Check fallbackStrategy (default:SKIP)│
│            - ERROR    → Fail immediately                                    │
│            - SKIP     → WARNING, continue to step 2                         │
│            - FALLBACK → Use fallbackEClass (MUST be set, else ERROR)        │
│                         Write fallbackEClass's discriminatorValue           │
│                         → DONE ✓                                            │
│                                                                             │
│    Note: The discriminatorPath replaces _type entirely. Type Mapping        │
│    Registry is our implementation of Jackson's @JsonTypeInfo + @JsonSubTypes│
│    pattern. See: 08-discriminator-mapping.md for full documentation.        │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. INLINE MAPPING (on EReference)                                           │
│    ─────────────────────────────────────────────────────────────────────────│
│    Is this a nested object (accessed via EReference)?                       │
│                                                                             │
│    NO (root object) → Continue to step 3                                    │
│                                                                             │
│    YES:                                                                     │
│      Is there an inlineMapping annotation on this EReference?               │
│                                                                             │
│      NO → Continue to step 3                                                │
│                                                                             │
│      YES:                                                                   │
│        → Get typeKey (configured on EReference or default _type)            │
│        → Get discriminatorValue for this EClass:                            │
│            Reverse lookup in inline mappings (EClass → value)               │
│        → Found? ──────────────────────────────────────────────────────────  │
│          │ YES → Write discriminatorValue at typeKey                        │
│          │       Do NOT proceed to Type Strategy ────────→ DONE ✓           │
│          │                                                                  │
│          └ NO (EClass not in inline mappings) → Check fallbackStrategy      │
│              (default: SKIP):                                               │
│              - ERROR    → Fail immediately                                  │
│              - SKIP     → WARNING, continue to step 3                       │
│              - FALLBACK → Use fallbackEClass (MUST be set, else ERROR)      │
│                           Write fallbackEClass's discriminatorValue         │
│                           → DONE ✓                                          │
│                                                                             │
│    Note: Inline mapping uses typeKey, not a custom path. It's a simpler,    │
│    more static variant of Type Mapping Registry.                            │
│    See: 08-discriminator-mapping.md section 5 for full documentation.       │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. TYPE STRATEGY RESOLUTION                                                 │
│    ─────────────────────────────────────────────────────────────────────────│
│    Get effective config from annotations + save options:                    │
│      - typeStrategy (default: URI)                                          │
│      - typeFormat (default: PLAIN)                                          │
│      - typeKey (default: _type)                                             │
│      - typeNameKey (default: type)      ← for STRUCTURED format             │
│      - typeSchemaKey (default: schema)  ← for STRUCTURED format             │
│      - smartCompression (default: false)                                    │
│                                                                             │
│    If typeStrategy = NONE:                                                  │
│      → Do NOT write any type field ──────────────────────→ DONE ✓           │
│                                                                             │
│    3a. CHECK CUSTOM VALUE WRITER                                            │
│                                                                             │
│        Is typeValueWriterName configured?                                   │
│        ├─ YES → Full delegation to custom TypeValueWriter service           │
│        │        - Has: JsonGenerator, EffectiveCodecConfig, DiagnosticCollector│
│        │        - Writer handles everything: value, format, keys, supertype │
│        │        - Framework does NOT apply format/strategy/smartCompression  │
│        │        → DONE ✓                                                    │
│        │                                                                    │
│        └─ NO → Use built-in value generation (strategy-dependent):          │
│            ┌─────────────────────────────────────────────────────────────┐  │
│            │ Strategy        │ Value                                     │  │
│            ├─────────────────┼───────────────────────────────────────────┤  │
│            │ URI             │ EcoreUtil.getURI(eClass).toString()       │  │
│            │ NAME            │ eClass.getName()                          │  │
│            │ CLASS           │ eClass.getInstanceClassName()             │  │
│            │                 │ → ERROR if null!                          │  │
│            │ NUMERIC         │ String.valueOf(eClass.getClassifierID())  │  │
│            │ SCHEMA_AND_TYPE │ schema: ePackage.getNsURI()               │  │
│            │                 │ type: eClass.getName()                    │  │
│            └─────────────────────────────────────────────────────────────┘  │
│                                                                             │
│    3b. Apply smartCompression (if enabled)                                  │
│        Is this a nested object AND same schema as root?                     │
│        ├─ YES → Use simple name instead of full URI                         │
│        └─ NO  → Use value from 3a as-is                                     │
│                                                                             │
│    3c. WRITE TYPE based on format                                           │
│        ┌─────────────────────────────────────────────────────────────────┐  │
│        │ Format + Strategy      │ Output                                 │  │
│        ├────────────────────────┼────────────────────────────────────────┤  │
│        │ PLAIN + URI/NAME/CLASS │ "_type": "<value>"                     │  │
│        │ PLAIN + NUMERIC        │ "_type": "<classifierID>"              │  │
│        │ PLAIN + SCHEMA_AND_TYPE│ "_schema": "...", "_type": "..."       │  │
│        │ STRUCTURED + any       │ "_type": { <nested object> }           │  │
│        └─────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│    If strategy fails (e.g., CLASS with null instanceClassName):             │
│      → ERROR (no fallback - user misconfiguration)                          │
│                                                                             │
│    3d. WRITE SUPERTYPE (if superTypeSerialize=true)                         │
│        See [SuperType Serialization](./07-supertype) for full details.     │
│                                                                             │
│        ┌─────────────────────────────────────────────────────────────────┐  │
│        │ Type Format  │ SuperType Output                                 │  │
│        ├──────────────┼──────────────────────────────────────────────────┤  │
│        │ PLAIN        │ Standalone "_supertype" field at root level      │  │
│        │ STRUCTURED   │ "supertype" field inside "_type" object          │  │
│        └─────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│        SuperType values use smart compression based on ROOT schema:         │
│        - Same namespace as root → simple name (e.g., "Entity")              │
│        - Different namespace → full URI (e.g., "http://other/1.0#//Base")   │
│                                                                             │
│        Note: SuperType is written even when discriminator mapping (steps    │
│        1-2) handled type resolution. Discriminator only affects _type,      │
│        not supertype information.                                           │
│                                                                             │
│    ──────────────────────────────────────────────────────→ DONE ✓           │
└─────────────────────────────────────────────────────────────────────────────┘

5.0.1 Serialization Priority Summary

PriorityMechanismConfigured OnApplies To
1Type Mapping RegistryEClass (base)All objects of that type
2Inline MappingEReferenceObjects accessed via that reference
3Type StrategyEClass, EReference, GlobalStandard type field writing

Note: Unlike deserialization, serialization has no step 4 (Fallback hints). If discriminator/inline mappings fail with fallbackStrategy=FALLBACK but no fallbackEClass, or if Type Strategy fails, it's an ERROR.

5.0.2 Runtime Ordering Constraint (idOnTop)

The type serialization flow above determines what type information to write. The position of this output relative to _id is controlled by the idOnTop property (see 09-id.md §8.7):

idOnTopMetadata Write Order
true_id_type_fingerprint_supertype → features
false (default)_type_fingerprint_supertype_id → features

This is a runtime orchestration constraint — the CodecEObjectSerializer evaluates idOnTop from the effective ID config and invokes the type and ID serialization entries in the appropriate order. The type flow itself does not need to know about idOnTop; the orchestrator handles the sequencing.

The fingerprint has a defined slot directly after the type (_id → _type → fingerprint → features) rather than a carrier mechanism of its own, because idOnTop is only an ordering constraint, not a preamble object — there is no separate metadata container it could live in. In STRUCTURED format it is written inside the type object, so the slot is intrinsic. See §8. The slot is only occupied when a fingerprint is actually due; by default nothing is written and the order above is unchanged from single-version output.

Deserialization: Field order is irrelevant — JSON objects are unordered. See Architecture §5.1.


6. Deserialization

6.1 Type Key Recognition

The deserializer automatically recognizes the following property names as type discriminators:

KeyDescription
_typeDefault type key (recommended)
_classAlternative type key
@typeJSON-LD compatible
eClassEMF/emfjson-jackson compatible

Alongside the type key, the deserializer recognises the fingerprint key — _fingerprint as a PLAIN sibling, fingerprint inside a STRUCTURED type object — plus any key configured through codec.fingerprintKey. It is a known metadata key, never unknown data: it must not trip strictOnUnknown and must not be misread as a data property. See §8.

Important: The property name type is NOT automatically recognized as a type key, because it is a very common attribute name in data formats like GeoJSON ("type": "Point"), OpenAPI-generated models, etc.

To use type as type discriminator, configure it explicitly:

java
CodecConfiguration config = CodecConfiguration.builder()
    .globalTypeKey("type")
    .build();

Or via EAnnotation on the EPackage:

xml
<eAnnotations source="http://eclipse.org/fennec/codec">
  <details key="typeKey" value="type"/>
</eAnnotations>

6.2 Format Detection

Deserializers detect the format from the JSON structure:

InputDetected FormatStrategy
"_type": "string"PLAIN (auto-detected)From effective config (NOT auto-detected)
"_type": { ... }STRUCTURED (auto-detected)From effective config (NOT auto-detected)
"_schema": ..., "_type": "string"PLAIN (auto-detected)SCHEMA_AND_TYPE (from config)

Important: Format is auto-detected from JSON structure, but strategy must be configured to match serialization. Strategy auto-detection is a future feature.

6.3 Type Resolution

The deserializer resolves the EClass based on a priority chain: discriminator mappings first, then type strategy, then fallback hints.

6.3.0 Type Resolution Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│                     TYPE DESERIALIZATION FLOW                               │
└─────────────────────────────────────────────────────────────────────────────┘

INPUT: JSON object, context (EReference if nested, load options)

┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. TYPE MAPPING REGISTRY (on EClass)                                        │
│    ─────────────────────────────────────────────────────────────────────────│
│    Is there a typeMapping annotation on the expected EClass (or its base)?  │
│                                                                             │
│    NO → Continue to step 2                                                  │
│                                                                             │
│    YES:                                                                     │
│      → Read value from discriminatorPath (e.g., "info.profileName")         │
│      → Lookup value in registry mappings                                    │
│      → Found? ────────────────────────────────────────────→ RESOLVED ✓      │
│      → Not found? Check fallbackStrategy (default: SKIP):                   │
│          - ERROR    → Fail immediately                                      │
│          - SKIP     → WARNING, continue to step 2                           │
│          - FALLBACK → Use fallbackEClass (MUST be set, else ERROR)          │
│                       → RESOLVED ✓                                          │
│                                                                             │
│    Note: Type Mapping Registry is our implementation of Jackson's           │
│    @JsonTypeInfo + @JsonSubTypes pattern. It's the primary, flexible        │
│    mechanism for discriminator-based type resolution.                       │
│    See: 08-discriminator-mapping.md for full documentation.                 │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. INLINE MAPPING (on EReference)                                           │
│    ─────────────────────────────────────────────────────────────────────────│
│    Is this a nested object (accessed via EReference)?                       │
│                                                                             │
│    NO (root object) → Continue to step 3                                    │
│                                                                             │
│    YES:                                                                     │
│      Is there an inlineMapping annotation on this EReference?               │
│                                                                             │
│      NO → Continue to step 3                                                │
│                                                                             │
│      YES:                                                                   │
│        → Read value from typeKey (configured on EReference or default)      │
│        → Lookup value in inline mappings                                    │
│        → Found? ──────────────────────────────────────────→ RESOLVED ✓      │
│        → Not found? Check fallbackStrategy (default: SKIP):                 │
│            - ERROR    → Fail immediately                                    │
│            - SKIP     → WARNING, continue to step 3                         │
│            - FALLBACK → Use fallbackEClass (MUST be set, else ERROR)        │
│                         → RESOLVED ✓                                        │
│                                                                             │
│    Note: Inline Mapping is a simpler, more static variant of Type Mapping   │
│    Registry. It's per-reference only and less flexible.                     │
│    See: 08-discriminator-mapping.md section 5 for full documentation.       │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. TYPE STRATEGY RESOLUTION                                                 │
│    ─────────────────────────────────────────────────────────────────────────│
│    Get effective config from annotations + load options:                    │
│      - typeStrategy (default: URI)      ← MUST match serialization config!  │
│      - typeFormat (default: PLAIN)      ← can be auto-detected from JSON    │
│      - typeKey (default: _type)                                             │
│      - typeNameKey (default: type)      ← for STRUCTURED format             │
│      - typeSchemaKey (default: schema)  ← for STRUCTURED format             │
│      - smartCompression (default: false)← MUST match serialization config!  │
│                                                                             │
│    If typeStrategy = NONE:                                                  │
│      → Skip type field processing entirely                                  │
│      → Go directly to step 4 (Fallback)                                     │
│                                                                             │
│    3a. CHECK CUSTOM VALUE READER                                            │
│                                                                             │
│        Is typeValueReaderName configured?                                   │
│        ├─ YES → Full delegation to custom TypeValueReader service           │
│        │        - Has: JsonParser, EffectiveCodecConfig, DiagnosticCollector│
│        │        - Reader handles everything: locate field, parse, resolve   │
│        │        - Framework does NOT apply format detection/strategy        │
│        │        → RESOLVED ✓                                                │
│        │                                                                    │
│        └─ NO → Continue with built-in logic (step 3b)                       │
│                                                                             │
│    3b. DETECT FORMAT from JSON structure (auto-detection OK)                │
│        - Type field value is STRING? → PLAIN                                │
│        - Type field value is OBJECT? → STRUCTURED                           │
│        - Type field not present? → Go to step 4 (Fallback)                  │
│                                                                             │
│    3c. EXTRACT TYPE VALUE (built-in, strategy-dependent)                    │
│                                                                             │
│            ┌─────────────────────────────────────────────────────────────┐  │
│            │ Strategy        │ Extraction                                │  │
│            ├─────────────────┼───────────────────────────────────────────┤  │
│            │ URI             │ Read single value from typeKey            │  │
│            │ NAME            │ Read single value from typeKey            │  │
│            │ CLASS           │ Read single value from typeKey            │  │
│            │ NUMERIC (PLAIN) │ Read single value from typeKey            │  │
│            │ NUMERIC (STRUCT)│ Read schema + classifier from nested obj  │  │
│            │ SCHEMA_AND_TYPE │ Read schema + type (two fields or nested) │  │
│            └─────────────────────────────────────────────────────────────┘  │
│                                                                             │
│    3c'. APPLY SMART COMPRESSION EXPANSION (if smartCompression = true)      │
│                                                                             │
│        Smart compression writes simple names for same-schema types.         │
│        On deserialization, we must expand simple names back to full URIs.   │
│        See: 05-global-options.md section 1.6 for full rules.                │
│                                                                             │
│        Is the extracted type value a SIMPLE NAME (no "#" or "://")?         │
│        ├─ YES → Expand using context schema:                                │
│        │        - Context schema from: root's URI, CODEC_ROOT_SCHEMA, etc.  │
│        │        - Result: contextSchema + "#//" + simpleName                │
│        │        - Example: "Person" → "http://example.org/1.0#//Person"     │
│        │                                                                    │
│        └─ NO (already full URI) → Use value as-is                           │
│                                                                             │
│        Note: This is the REVERSE of serialization step 3b in section 5.     │
│        If serialization compressed "http://example.org/1.0#//Person" to     │
│        "Person", deserialization expands it back.                           │
│                                                                             │
│    3d. RESOLVE EClass based on CONFIGURED strategy (NO auto-detection!)     │
│                                                                             │
│        Note: If smartCompression was applied in 3c', value is now a         │
│        full URI. The strategy-based resolution below handles both cases.    │
│                                                                             │
│        Strategy-based resolution:                                           │
│        ┌─────────────────────────────────────────────────────────────────┐  │
│        │ Strategy        │ Resolution                                    │  │
│        ├─────────────────┼───────────────────────────────────────────────┤  │
│        │ URI             │ Parse full URI (scheme://...#//ClassName)     │  │
│        │ NAME            │ Simple name + context schema                  │  │
│        │ CLASS           │ Lookup by instanceClassName                   │  │
│        │ NUMERIC         │ Classifier ID + hint package (REQUIRED!)      │  │
│        │ SCHEMA_AND_TYPE │ Compose URI from extracted schema + name      │  │
│        └─────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│    Resolved? ─────────────────────────────────────────────→ RESOLVED ✓      │
│    Not resolved? → Continue to step 4                                       │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. FALLBACK (see section 6.5.1 for TypeHintMode details)                    │
│    ─────────────────────────────────────────────────────────────────────────│
│    Check CODEC_TYPE_HINT_MODE (default: HINT)                               │
│                                                                             │
│    ┌─────────────────────────────────────────────────────────────────────┐  │
│    │ HINT MODE (default)                                                 │  │
│    │ ───────────────────────────────────────────────────────────────────│  │
│    │ Try in order:                                                       │  │
│    │   1. EReference.getEReferenceType() (if concrete)                   │  │
│    │   2. CODEC_FEATURE_TYPE_HINTS (per-feature runtime hint)            │  │
│    │   3. CODEC_ROOT_TYPE (root type hint)                               │  │
│    │                                                                     │  │
│    │ If resolved → log WARNING, RESOLVED ✓                               │  │
│    │ If reference type is abstract and no hints resolve → step 4c        │  │
│    └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│    ┌─────────────────────────────────────────────────────────────────────┐  │
│    │ OVERRIDE MODE                                                       │  │
│    │ ───────────────────────────────────────────────────────────────────│  │
│    │ Try in order (reference type IGNORED):                              │  │
│    │   1. CODEC_FEATURE_TYPE_HINTS (per-feature runtime hint)            │  │
│    │   2. CODEC_ROOT_TYPE (root type hint)                               │  │
│    │                                                                     │  │
│    │ If resolved → log WARNING, RESOLVED ✓                               │  │
│    │ If neither hint is set → ERROR (fail, no implicit fallback)         │  │
│    └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│    4c. FINAL FAILURE (no fallback resolved)                                 │
│                                                                             │
│        Is this the ROOT object?                                             │
│        ├─ YES → ERROR (mandatory - cannot deserialize without root type)    │
│        │        DeserializationMode doesn't apply here                      │
│        │                                                                    │
│        └─ NO (nested object) → Check DeserializationMode:                   │
│             ├─ LENIENT (default) → WARNING + skip:                          │
│             │    - Single reference: set to null                            │
│             │    - Multi reference: don't add element to collection         │
│             │                                                               │
│             └─ STRICT → ERROR                                               │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ 5. SUPERTYPE PARSING (after type resolution complete)                       │
│    ─────────────────────────────────────────────────────────────────────────│
│    See [SuperType Serialization](./07-supertype) section 8 for full details.│
│                                                                             │
│    SuperType parsing is OPTIONAL and does NOT affect type resolution.       │
│    The type (EClass) is already resolved from steps 1-4.                    │
│                                                                             │
│    If type field was detected as PLAIN format:                              │
│      → Look for standalone supertype field (superTypeKey, default _supertype)│
│      → Parse if present (array or separator-joined string)                  │
│                                                                             │
│    If type field was detected as STRUCTURED format:                         │
│      → Look for supertype inside _type object (key: supertype)              │
│      → Parse if present (array or separator-joined string)                  │
│                                                                             │
│    Validation (based on DeserializationMode):                               │
│      ┌─────────────────────────────────────────────────────────────────────┐│
│      │ LENIENT (default) │ Parse supertype, IGNORE values (no validation)  ││
│      │ STRICT            │ Validate declared supertypes match EClass       ││
│      │                   │ hierarchy. Fail if mismatch.                    ││
│      │ AUTO_DETECT       │ Same as LENIENT for supertype                   ││
│      └─────────────────────────────────────────────────────────────────────┘│
│                                                                             │
│    Note: SuperType parsing happens even if discriminator mapping (steps 1-2)│
│    resolved the type. SuperType is independent of how type was resolved.    │
└─────────────────────────────────────────────────────────────────────────────┘

6.3.1 Resolution Priority Summary

PriorityMechanismConfigured OnApplies To
1Type Mapping RegistryEClass (base)All objects of that type
2Inline MappingEReferenceObjects accessed via that reference
3Type StrategyEClass, EReference, GlobalStandard type field resolution
4FallbackLoad optionsLast resort when above fail

Design rationale: Type Mapping Registry has highest priority because it's our implementation of Jackson's @JsonTypeInfo + @JsonSubTypes pattern - the primary, flexible mechanism for polymorphic type handling. Inline Mapping is a simpler, more static variant for per-reference cases.

Important - Symmetry requirement: The typeStrategy and smartCompression settings must match between serialization and deserialization. Format can be auto-detected, but strategy cannot.

Future feature: Auto-detection of strategy from content is not currently supported.

Example (PLAIN + URI):

json
{
  "_type": "http://example.org/person/1.0#//Person",
  "name": "John"
}

The deserializer extracts http://example.org/person/1.0 as namespace URI and Person as class name.

Example (STRUCTURED + SCHEMA_AND_TYPE):

json
{
  "_type": {
    "schema": "http://example.org/person/1.0",
    "type": "Person"
  },
  "name": "John"
}

6.3.2 Unknown Type Handling

When the deserializer cannot resolve a type value to a known EClass, the behavior depends on whether a type hint is available.

Fallback to Type Hint

If a CODEC_ROOT_TYPE hint is provided (or the reference type is concrete), the deserializer uses it as fallback:

ScenarioBehaviorSeverity
Unknown type value, hint availableUse hint, continueWARNING
Unknown type value, no hintFailERROR

Example - Unknown type with hint (succeeds):

java
Map<String, Object> options = Map.of(
    CodecResourceOptions.CODEC_ROOT_TYPE, PersonPackage.Literals.PERSON
);
resource.load(inputStream, options);
// JSON: {"_type": "UnknownType", "name": "John"}
// Result: WARNING logged, Person created using hint

Example - Unknown type without hint (fails):

java
resource.load(inputStream, Collections.emptyMap());
// JSON: {"_type": "UnknownType", "name": "John"}
// Result: ERROR - "Cannot deserialize: no type information found and no CODEC_ROOT_TYPE hint"

Resolution Order

When resolving type values, the deserializer tries multiple strategies:

  1. Full URI - If value matches EMF URI pattern (scheme://...#//ClassName), resolve directly
  2. Java class name - If value contains ., look up by instance class name
  3. Discriminator mapping - Check TypeDiscriminatorService for mapped values
  4. Simple name - Look up by class name in context schema
  5. Numeric ID - Parse as classifier ID within context schema
  6. Fallback - Use hint if available, otherwise ERROR

Diagnostic Messages

ScenarioMessage
Type value not resolvedCould not resolve EClass from type value: {value}
No type info and no hintCannot deserialize: no type information found and no CODEC_ROOT_TYPE hint

See Error Handling for the complete error scenarios table.

6.4 Context Schema and NAME Strategy

The NAME strategy writes only the simple EClass name (e.g., "Person" instead of "http://example.org/1.0#//Person"). For deserialization, this requires a Context Schema to resolve the simple name back to a full EClass URI.

6.4.1 CODEC_ROOT_SCHEMA Option

Explicitly sets the context schema for deserialization:

java
Map<String, Object> options = new HashMap<>();
options.put(CodecResourceOptions.CODEC_ROOT_SCHEMA, "http://geojson.org/1.0");
resource.load(inputStream, options);

With this option, all simple type names are resolved against the specified schema:

  • "type": "Point"http://geojson.org/1.0#//Point
  • "type": "Feature"http://geojson.org/1.0#//Feature

Example (GeoJSON-like):

json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": {"longitude": 8.68, "latitude": 50.11}
      }
    }
  ]
}

6.4.2 CODEC_ROOT_TYPE Option

Specifies the expected root EClass for deserialization. Additionally, it implicitly sets the context schema from the EClass's package URI.

java
Map<String, Object> options = new HashMap<>();
options.put(CodecResourceOptions.CODEC_ROOT_TYPE, GeoJsonPackage.eINSTANCE.getFeatureCollection());
resource.load(inputStream, options);

This provides two benefits:

  1. Root object type is known → no _type needed at root level
  2. Context schema is established → nested objects can use simple names

Example (no _type at root needed):

json
{
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": {"longitude": 8.68, "latitude": 50.11}
      }
    }
  ]
}

6.4.3 Context Schema Resolution Order

SourcePriorityDescription
CODEC_ROOT_SCHEMA1 (highest)Explicitly provided schema URI
CODEC_ROOT_TYPE2Extracted from hint EClass's package
First full URI in content3Smart compression: schema from root's _type

Security (S-4): Without a context schema, NAME, CLASS, and NUMERIC strategies fail with a warning diagnostic instead of scanning all registered EPackages. This prevents type confusion when multiple packages define classes with the same name. Always provide CODEC_ROOT_SCHEMA or CODEC_ROOT_TYPE when using these strategies.

NAME strategy uses MetadataIndexReader.findByClassName(nsURI, className) for context-aware lookup. See CLASS Strategy Resolution for the full MetadataIndex API.

6.4.4 Smart Compression

When the root object uses a full URI, the schema is automatically extracted and used for nested objects. With several resource roots, each root establishes its own context and writes a full URI — see 05 §1.2.1:

json
{
  "_type": "http://example.org/company/1.0#//Company",
  "name": "Acme Inc",
  "employees": [
    {"_type": "Person", "name": "Alice"},
    {"_type": "Person", "name": "Bob"}
  ]
}

The deserializer:

  1. Parses root _type: http://example.org/company/1.0#//Company
  2. Extracts context schema: http://example.org/company/1.0
  3. Resolves "Person"http://example.org/company/1.0#//Person

Important: Smart compression requires symmetric configuration. If content was serialized with smartCompression=true, deserialization must also use smartCompression=true. A mismatch produces a WARNING. See Global Options - Smart Compression for details.

6.4.5 CLASS Strategy Resolution

The CLASS strategy writes the Java class name (EClass.getInstanceClassName()) as the type value. Deserialization must reverse this lookup.

Serialization
java
// Writes: "_type": "org.example.model.impl.PersonImpl"
String typeValue = eClass.getInstanceClassName();

Requirement: EClass.getInstanceClassName() must be non-null. This is typical for EMF-generated code. If null, serialization fails with ERROR.

Deserialization

CLASS strategy resolution uses the MetadataService to lookup EClass by instanceClassName. The resolution follows the context schema pattern (same as NAME strategy):

ROOT OBJECT:

1. Check CODEC_ROOT_TYPE hint → extract context package nsURI
2. Check CODEC_ROOT_SCHEMA hint → use as context package nsURI
3. If context nsURI exists:
   → MetadataService.findByInstanceClassName(nsUri, className)
   → If found → RESOLVED ✓
4. No hint or not found in context package:
   → WARNING diagnostic: "CLASS strategy requires CODEC_ROOT_SCHEMA or CODEC_ROOT_TYPE"
   → Resolution returns null (S-4: no global EPackage scan)

NESTED OBJECT:

1. Get context from EReference.getEReferenceType().getEPackage().getNsURI()
2. MetadataService.findByInstanceClassName(contextNsUri, className)
3. If found → RESOLVED ✓
4. If not found in context package:
   → WARNING diagnostic: resolution fails (S-4: no global EPackage scan)
Ambiguous Resolution (Multiple Matches)

When multiple EClasses share the same instanceClassName (common with typed maps like java.util.Map$Entry), and no context schema narrows the search:

  • Severity: ERROR
  • Message: Ambiguous instanceClassName "{className}": found in multiple packages: [{uri1}, {uri2}, ...]. Use CODEC_ROOT_SCHEMA to disambiguate.

This is a standard EMF pattern (typed map entries), so we cannot treat duplicate instanceClassName as a configuration error. Instead, users must provide context via CODEC_ROOT_SCHEMA or CODEC_ROOT_TYPE.

Recommendation

Strongly recommend using CODEC_ROOT_SCHEMA or CODEC_ROOT_TYPE with CLASS strategy:

java
// Good: Context provided, unambiguous resolution
Map<String, Object> options = Map.of(
    CodecResourceOptions.CODEC_ROOT_SCHEMA, "http://example.org/model/1.0"
);
resource.load(inputStream, options);

// Risky: No context, may fail if className exists in multiple packages
resource.load(inputStream, Collections.emptyMap());
MetadataIndex API

The MetadataIndexReader (accessed via MetadataService.getIndexReader()) provides indexed lookup for CLASS strategy resolution:

java
interface MetadataIndexReader {
    // Lookup by instanceClassName within a specific package (context-aware)
    ClassMetadata findByInstanceClassName(String nsURI, String instanceClassName);

    // Lookup across all registered packages (may return multiple)
    EList<ClassMetadata> findAllByInstanceClassName(String instanceClassName);

    // Similar methods for NAME strategy
    ClassMetadata findByClassName(String nsURI, String className);
    EList<ClassMetadata> findAllByClassName(String className);

    // URI strategy uses direct lookup
    ClassMetadata findClassByURI(String uri);
}

Usage Example:

java
MetadataService metadataService = ...; // OSGi service or injected
MetadataIndexReader index = metadataService.getIndexReader();

// Context-aware lookup (preferred)
String contextNsURI = eReference.getEReferenceType().getEPackage().getNsURI();
ClassMetadata meta = index.findByInstanceClassName(contextNsURI, "org.example.PersonImpl");

// Global search (when no context available)
EList<ClassMetadata> matches = index.findAllByInstanceClassName("org.example.PersonImpl");
if (matches.size() == 1) {
    // Unambiguous
} else if (matches.size() > 1) {
    // ERROR: Ambiguous, need CODEC_ROOT_SCHEMA
}

The index is built automatically when EPackages are registered via MetadataService.registerPackage(). Current implementation (MapBasedMetadataIndex) uses in-memory ConcurrentHashMaps; future versions may use Lucene for large-scale deployments.

6.4.6 Package Resolution Order (multi-version) [B.5 / A.3]

When the codec resolves an nsURI (from a nsURI#//Class type URI, a context schema, or a discriminator URI) to a concrete EPackage/PackageMetadata version, the source order is binding:

TierSourceNotes
1Per-load pinIf this nsURI was already resolved in this load, reuse that version (consistency + no repeated lookup).
2ResourceSet package registrygetResourceSet().getPackageRegistry() — caller context with concrete instances; instance-precise, no ambiguity.
3MetadataService candidate querygetPackageMetadataVersions(nsURI) + the count rule (A.3 below); a codec.rootFingerprint (or, later, in-band fingerprint) resolves directly via getPackageMetadataByFingerprint.
4EPackage.Registry.INSTANCEOnly for an nsURI the MetadataService does not know at all (true foreign / plain-EMF packages). Hard rule: for an nsURI the service does know, the global registry is never consulted — it holds only one version per nsURI and would reopen the back door around version selection.

The selected version is pinned for the nsURI for the remainder of the load (tier 1 on subsequent hits). In the single-version case all tiers agree with prior behavior — only the source order becomes binding (R1).

A.3 — count-based candidate rule (tier 3, no trial). When resolving an nsURI without a disambiguating fingerprint (neither codec.rootFingerprint nor a pin):

# registered versions for the nsURIBehavior (both strictness modes)
0not known to the MetadataService → fall through to tier 4 (foreign package)
exactly 1use it — the normal single-version case; no cost, no behavior change (R1)
> 1error, listing the candidate fingerprints; the caller disambiguates via codec.rootFingerprint (or an instance root option). No trial, no auto-pick, in every mode.

The candidate query is scoped to one nsURI — it is a bounded lookup, not the global cross-package scan that S-4 forbids (§15 9.3). The MetadataService.getClassMetadataByURI last-wins behavior is therefore not used for version selection under multi-version; the count rule replaces it.

6.5 Type Resolution Rules

Content has _typeCODEC_ROOT_TYPE setBehavior
Yes (full URI)NoUse content type, establish context schema
Yes (simple name)NoResolve via context schema; WARNING if no schema (S-4)
YesYesUse content type, context schema from hint
NoYesUse hint type, context schema from hint
NoNoERROR: Cannot determine type

When both content type and hint are present but differ:

  • Log a WARNING
  • Continue with content type (content wins)
  • Example: Hint says Person, content says Employee → use Employee, log warning

6.5.1 Type Hint Mode (CODEC_TYPE_MODE)

By default, CODEC_ROOT_TYPE and CODEC_ROOT_SCHEMA behave as hints/fallbacks, not as highest-priority overrides. This is an intentional deviation from the standard configuration hierarchy (where Load/Save options have highest priority).

Rationale:

  • Most common use case is "help deserialize when type info is missing"
  • Users expect content type information to be respected
  • Forcing override by default would cause unexpected behavior
ModeBehaviorUse Case
HINT (default)Content type wins when present; hint used as fallbackGeneral deserialization, flexible input handling
OVERRIDEHint always wins for root object, ignoring content typeForce-deserialize as specific type regardless of content

HINT Mode (default):

Type Resolution Priority:
1. Content type information (from JSON _type field) - wins when present
2. CODEC_ROOT_TYPE / CODEC_ROOT_SCHEMA - used as fallback when content has no type
3. Reference type (EReference.getEReferenceType()) - for nested objects
4. ERROR - if no type determinable

OVERRIDE Mode:

Type Resolution Priority:
1. CODEC_ROOT_TYPE - always wins for root object (follows strict config hierarchy)
2. Content type information - ignored for root object
3. Reference type - for nested objects (CODEC_ROOT_TYPE only affects root)

Configuration:

java
Map<String, Object> options = new HashMap<>();
options.put(CodecResource.CODEC_ROOT_TYPE, PersonPackage.Literals.PERSON);
options.put(CodecResource.CODEC_TYPE_MODE, TypeHintMode.OVERRIDE);  // Force type
resource.load(inputStream, options);

Example - HINT mode (default):

java
// JSON has type info - content wins, hint ignored
String json = """
    { "_type": "http://example.org#//Employee", "name": "John" }
    """;

options.put(CODEC_ROOT_TYPE, PersonPackage.Literals.PERSON);
// Result: Deserializes as Employee (content wins)
// If Employee is subtype of Person: no warning
// If incompatible: WARNING logged but content still wins

Example - OVERRIDE mode:

java
// Force deserialize as specific type, ignore content type
String json = """
    { "_type": "http://example.org#//OldType", "name": "John" }
    """;

options.put(CODEC_ROOT_TYPE, PersonPackage.Literals.PERSON);
options.put(CODEC_TYPE_MODE, TypeHintMode.OVERRIDE);
// Result: Deserializes as Person (override mode, content type ignored)

Note: This behavior deviates from the general configuration hierarchy principle where Load/Save options (Level 1) have highest priority. The deviation exists for practical usability reasons.

6.5.2 Deserialization Mode (Type Resolution Strictness)

The DeserializationMode controls how strictly the deserializer follows the configured type strategy when resolving types.

ModeBehaviorUse Case
STRICTType field MUST match configured strategy exactly; missing/malformed → ERRORGuaranteed format compliance
LENIENT (default)Try configured strategy first, then fallback resolutionMaximum interoperability
AUTO_DETECTIgnore configured strategy; probe JSON structure to determine formatUnknown/mixed formats

Why LENIENT is default:

  • Most users want deserialization to succeed when possible
  • Real-world JSON often has minor variations
  • STRICT is available for those who need format guarantees
  • LENIENT + warnings provides best of both worlds

STRICT Mode:

java
// Config says SCHEMA_AND_TYPE, but JSON only has simple name → ERROR
CodecConfiguration config = CodecConfiguration.builder()
    .typeStrategy(TypeStrategy.SCHEMA_AND_TYPE)
    .deserializationMode(DeserializationMode.STRICT)
    .build();

// JSON: {"_type": "Person", "name": "John"}  // No _schema field
// Result: ERROR - "Missing schema field, config requires SCHEMA_AND_TYPE"

LENIENT Mode (default):

java
// Config says SCHEMA_AND_TYPE, but JSON only has simple name → try fallback
CodecConfiguration config = CodecConfiguration.builder()
    .typeStrategy(TypeStrategy.SCHEMA_AND_TYPE)
    // .deserializationMode(DeserializationMode.LENIENT)  // default
    .build();

// JSON: {"_type": "Person", "name": "John"}
// Result: WARNING logged, resolves "Person" using CODEC_ROOT_SCHEMA or hint

AUTO_DETECT Mode:

java
// Ignore config, probe JSON structure
CodecConfiguration config = CodecConfiguration.builder()
    .typeStrategy(TypeStrategy.URI)  // Config says URI
    .deserializationMode(DeserializationMode.AUTO_DETECT)
    .build();

// JSON: {"_type": "Person", "name": "John"}  // Simple name
// Result: Auto-detects NAME format, resolves using context

Resolution Behavior by Mode:

ScenarioSTRICTLENIENTAUTO_DETECT
Missing type fieldERRORUse hintsUse hints
Wrong formatERRORTry fallbacksProbe format
Unknown type valueERRORWARNING + fallbackWARNING + fallback

Warning Messages (LENIENT mode):

WARNING: Type resolved via fallback. Config: typeStrategy=SCHEMA_AND_TYPE,
         but type "Person" resolved using CODEC_ROOT_SCHEMA hint.
         Consider updating config or JSON to match.

Note: DeserializationMode controls type resolution strictness. For feature handling strictness (unknown/missing features), see Feature Strictness.

6.6 Field Order Independence

JSON field order is irrelevant for deserialization. The _type field can appear anywhere in the object - data fields appearing before it are deferred and processed after type resolution.

See also: Architecture (section 5.1 - Deferred Properties) for the general mechanism that applies to all metadata fields (_type, _id, etc.).

6.7 Polymorphic Containment References

When an EReference points to an abstract EClass, the concrete type must be specified in the JSON:

Model:

Geometry (abstract)
  ├── Point
  ├── LineString
  └── Polygon

Feature
  └── geometry: Geometry (containment)

JSON:

json
{
  "_type": "http://geojson.org/1.0#//Feature",
  "geometry": {
    "_type": "http://geojson.org/1.0#//Point",
    "coordinates": {"longitude": 8.68, "latitude": 50.11}
  }
}

With context schema (NAME strategy):

json
{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": {"longitude": 8.68, "latitude": 50.11}
  }
}

The deserializer:

  1. Gets the reference type (Geometry) as a hint
  2. Reads _type/type from the nested object
  3. Resolves concrete class (Point) which must be a subtype of the hint
  4. If no _type and reference type is concrete: uses reference type
  5. If no _type and reference type is abstract: ERROR

6. Real-World Example: GeoJSON

This section demonstrates deserialization of real GeoJSON data using the org.geojson.model EMF model.

6.1 The GeoJSON Model

The GeoJSON model uses several codec-relevant features:

  1. type as discriminator - GeoJSON uses "type": "Point" instead of "_type"
  2. ExtendedMetaData for feature names - The data attribute maps to JSON key coordinates
  3. Array data types - Coordinates are double[], double[][], double[][][] arrays
  4. Polymorphic containment - Feature.geometry can be Point, LineString, Polygon, etc.

Model structure:

GeoJsonObject (abstract)
  └── bbox: double[] (via ExtendedMetaData: "bbox")

Geometry (interface, extends GeoJsonObject)
  ├── Point
  │     └── data: double[] (via ExtendedMetaData: "coordinates")
  ├── LineString
  │     └── data: double[][] (via ExtendedMetaData: "coordinates")
  ├── Polygon
  │     └── data: double[][][] (via ExtendedMetaData: "coordinates")
  └── ...

Feature (extends GeoJsonObject)
  ├── id: String
  ├── geometry: Geometry (containment)
  └── properties: EObject (containment)

FeatureCollection (extends GeoJsonObject)
  └── features: Feature[*] (containment)

6.2 Configuration

To deserialize real GeoJSON, configure:

java
CodecConfiguration config = CodecConfiguration.builder()
    .typeKey("type")                        // GeoJSON uses "type" not "_type"
    .useNamesFromExtendedMetaData(true)     // Map "coordinates" → data attribute
    .build();

Map<String, Object> options = new HashMap<>();
options.put(CodecResource.CODEC_ROOT_SCHEMA, "https://geojson.org/model/2016");

resource.load(inputStream, options);

6.3 Example: Point

GeoJSON Input:

json
{
  "type": "Point",
  "coordinates": [8.6821, 50.1109]
}

Deserialization:

  1. "type": "Point" → resolves to GeoJsonPackage.eINSTANCE.getPoint()
  2. "coordinates" → maps to data attribute (via ExtendedMetaData)
  3. [8.6821, 50.1109] → deserialized as double[]

6.4 Example: Polygon with BBox

GeoJSON Input:

json
{
  "type": "Polygon",
  "bbox": [11.504, 50.895, 11.567, 50.913],
  "coordinates": [
    [
      [11.554, 50.901],
      [11.554, 50.901],
      [11.554, 50.900],
      [11.554, 50.901]
    ]
  ]
}

Deserialization:

  1. "type": "Polygon"Polygon EClass
  2. "bbox"double[4] bounding box array
  3. "coordinates"double[][][] (rings containing coordinate arrays)

6.5 Example: FeatureCollection

GeoJSON Input:

json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "id": "berlin",
      "geometry": {
        "type": "Point",
        "coordinates": [13.405, 52.52]
      },
      "properties": null
    },
    {
      "type": "Feature",
      "id": "frankfurt",
      "geometry": {
        "type": "Point",
        "coordinates": [8.682, 50.110]
      },
      "properties": null
    }
  ]
}

Deserialization:

  1. Root: FeatureCollection with 2 features
  2. Each feature: polymorphic geometry resolved by nested "type" field
  3. Coordinates deserialized as double[] arrays

6.6 Key Takeaways

GeoJSON RequirementCodec Configuration
type as discriminator.typeKey("type")
coordinatesdata.useNamesFromExtendedMetaData(true)
Context schema for NAMECODEC_ROOT_SCHEMA option
Array coordinatesAutomatic (see Feature Serialization - Array Attributes)

7. Configuration Validation Rules

This section documents validation rules for Type Configuration that are checked when building the effective configuration.

7.1 Property Applicability Rules

Rule IDPropertyInvalid AtSeverityDescription
T-V1typeValueReaderNameEReferenceERRORValue reader is class-intrinsic (applies to objects of that EClass, not reference-specific)
T-V2typeValueWriterNameEReferenceERRORValue writer is class-intrinsic (applies to objects of that EClass, not reference-specific)
T-V3typeScopeEAnnotationWARNINGRuntime-only property, ignored if found in EAnnotation
T-V4typeFormatScopeEAnnotationWARNINGRuntime-only property, ignored if found in EAnnotation
T-V5Any type* keyEAttributeERRORType configuration not applicable to attributes
T-V6typeDiscriminatorPathEReferenceERRORDiscriminator path is per-class (see 08-discriminator-mapping.md)
T-V7typeDiscriminatorEReferenceERRORDiscriminator value is per-class (see 08-discriminator-mapping.md)

7.2 Strategy-Specific Rules

Rule IDConditionSeverityDescription
T-V10CLASS strategy + instanceClassName is nullERRORCLASS strategy requires EClass.getInstanceClassName() to be set

7.3 Format-Dependent Warnings

Rule IDConditionSeverityDescription
T-V20typeNameKey set + format=PLAINWARNINGtypeNameKey is only used in STRUCTURED format; value ignored
T-V21typeSchemaKey set + format=PLAIN + strategy≠SCHEMA_AND_TYPEWARNINGtypeSchemaKey is only used in STRUCTURED format or PLAIN+SCHEMA_AND_TYPE; value ignored

7.4 Serialization/Deserialization Symmetry

These properties must be configured symmetrically for serialization and deserialization to work correctly:

PropertyAuto-Detectable?Symmetry RequiredMismatch Behavior
typeStrategy❌ NO✅ YES - must matchType resolution fails or produces wrong EClass
typeFormat✅ YES (from JSON structure)❌ NON/A - auto-detected
smartCompression❌ NO✅ YES - must matchWARNING, type may not resolve correctly
typeKey❌ NOShould matchDeserializer auto-recognizes common keys (_type, @type, _class, eClass)

Future feature: Auto-detection of typeStrategy and smartCompression from content is not currently supported.


8. In-Band EPackage Fingerprint

An nsURI identifies a model, not a version of it. When several versions of the same nsURI are registered at once, nsURI alone no longer says which EClass instance a document means. The fingerprint is the version-precise identity of an EPackage, and this section defines how a document can carry it so that a reader can pick the right version from the data alone — a self-describing document.

The communicated currency is always the EPackage fingerprint, never a class-level or feature-level one. A version is selected per package; the configuration for the classes in it then follows from that selection.

8.1 When the Fingerprint Is Not Needed

Most documents never need it, and for them nothing changes:

  • No fingerprint in the data → resolve by nsURI. Exactly one version registered → use it. Done. This is the single-version case and it stays trivially simple.
  • The caller can also answer the version question out-of-band with codec.rootFingerprint, without touching the document.

The in-band carrier is therefore off by default (fingerprintMode=NONE) and must be opted into. Writing it by default would change the output of every existing document for a problem most callers do not have.

8.2 Carrier: A Citizen of the Type Context

The fingerprint is not a fourth serialization mechanism. It attaches to the type context that already exists:

  • PLAIN — a sibling key next to the type key, default _fingerprint:
    json
    {
      "_type": "http://example.org/model#//Person",
      "_fingerprint": "fp1:9f2c4e…",
      "name": "Mark"
    }
  • STRUCTURED — an inner key inside the type object, default fingerprint:
    json
    {
      "_type": { "type": "Person", "fingerprint": "fp1:9f2c4e…" },
      "name": "Mark"
    }
  • idOnTop and other ordering modes — the fingerprint keeps its slot directly after the type (see §5.0.2).

The key is configured with one value, fingerprintKey, whose configured form is the inner key; the PLAIN sibling derives from it by prefixing _ (_/@-prefixed values are taken as-is). This is the same one-value-two-placements rule as typeSchemaKey — see 03-naming-conventions §2.

A foreign reader that does not know the fingerprint simply sees one more property and can ignore it.

8.3 Write: Conservative, at First Touch

When fingerprintMode=FIRST_TOUCH, the fingerprint is written at the first occurrence of each distinct EPackage instance in document order — and only there:

  1. the root, for the root's package;
  2. every site where a new package instance enters the document — supertype substitution, reference or containment targets from another package;
  3. every site whose package instance deviates from the pin already established for its nsURI — the mixed-version marker that makes the deviation visible at all.

Repeating it on every object would be pure redundancy: once a version is established for an nsURI, every later object of that package is interpreted under it. There is deliberately no "write everywhere" variant.

Writer first-touch and reader pinning are the same rule seen from both sides — which is what makes the round-trip closed: the writer emits exactly the points at which the reader would otherwise have to guess.

Interaction with type omission. Smart compression and reference type omission may drop the type of an object whose type equals the declared reference type. Where a fingerprint is due under the rules above, the type context must not be omitted — otherwise the carrier would have nowhere to live and the document would silently lose the version information it was asked to record. A due fingerprint therefore suppresses the omission, not the other way round.

Deliberately no carrier. Two cases go the other way, because there the caller removed the type context on purpose rather than as an optimization:

  • typeStrategy=NONE — type information is suppressed entirely, so there is no type context and no fingerprint is written.
  • a nested typeDiscriminatorPath — the type is expressed by data at a feature path, and no type object is written to attach a fingerprint to.

A document produced this way is not self-describing; callers needing that must leave the type context in place, or answer the version question out-of-band with codec.rootFingerprint.

8.4 Read: Liberal

Reading is deliberately more permissive than writing: a reader accepts a fingerprint wherever it appears in any of the type-context locations of §8.2, regardless of how the document was produced or of the local write configuration. No pre-scan is needed — the fingerprint is read at the same point the type is read today, which also covers polymorphic sub-trees with per-object types.

A resolved fingerprint pins the version for its nsURI for the rest of that load: later objects of the same nsURI without their own fingerprint use the pinned version. A new nsURI appearing mid-document selects on the fly and is then pinned in turn.

Absence of a fingerprint means exactly what it meant before this feature existed — nsURI-based resolution — so documents written without it keep working unchanged.

8.5 ⚠ The fingerprintKey Chicken-and-Egg Problem

This is a genuine cycle, and it is broken deliberately. Do not "fix" it.

Reading liberally requires knowing which key carries the fingerprint before the version is selected. But a key configured through a model annotation lives in the configuration that only exists after a version has been selected — and selecting the version is precisely what the fingerprint is for. Resolving the read key from the model would therefore require the answer in order to find the question.

The break:

DirectionWhere fingerprintKey comes from
ReadCaller-side sources only — options, resource, factory, module. The default key (_fingerprint / fingerprint) is always accepted in addition.
WriteAll configuration levels, including the model annotation.

So the annotation level configures the key for writing only, never for reading. A document written with a custom, annotation-configured key is readable only by a caller who supplies that same key out-of-band — which is a documented consequence, not an oversight. Callers who need self-describing documents should keep the default key.

Anyone tempted to make the read side "consistent" by resolving the key from the model reintroduces the paradox.

8.6 Strictness and Unknown Fields

The fingerprint key is a known key, not unknown data: strictOnUnknown must accept it and must never reject a document because of it. Likewise, the STRUCTURED reference parser must recognise the key inside a type object — otherwise it is misclassified as projection or orphan data (see 10-reference.md).

For what happens when a fingerprint is present but cannot be resolved, when it contradicts codec.rootFingerprint, or when versions collide mid-document, see 13-load-save-options.md (signal contract) and 15-error-handling.md (error catalog).

8.7 Configuration

KeyLevelsDirectionDefaultMeaning
fingerprintModeG, P, CWNONENONE = write no fingerprint; FIRST_TOUCH = write it per §8.3
fingerprintKeyG, P, CRW (read: caller-side only, §8.5)fingerprint (PLAIN: _fingerprint)Key carrying the fingerprint

On/off is expressed through fingerprintMode rather than a separate boolean flag, mirroring typeStrategy=NONE (a parallel typeInclude boolean is deprecated for exactly this reason — see 16-annotation-reference.md).

8.8 Format Applicability

The carrier needs a place to put a named property next to the type, so it applies to property-stream formats and not to column formats:

Format familyIn-band carrierHow the version question is answered
JSON, YAML, CBOR, BSONIn-band fingerprint (this section) or codec.rootFingerprint
CSV, XLSX, ODS, tabularcodec.rootFingerprint on load only

A column format would have to turn the fingerprint into a column repeated on every row, and the tabular shape has no per-object type context to attach it to. Such formats report supportsInBandFingerprint() == false; the codec then suppresses the carrier with a warning even if fingerprintMode asks for it — writing a column no reader of that format can interpret would be worse than writing nothing, and silently ignoring an explicit request is what this design avoids everywhere else.

Conformance is checked by a shared TCK suite that every property-stream format extends, so a new format cannot quietly omit the carrier. See format-tck-capability-map.md row 49.

XMI never carries a fingerprint. A document round-tripped through XMI loses the in-band version information; the version has to come from the caller on the way back in.

Package-level opt-in is the natural one. The fingerprint's currency is the EPackage, so an annotation on the package is where this configuration belongs — one opt-in for every class in the model, instead of repeating it per class:

xml
<ecore:EPackage name="orders" nsURI="http://example.org/orders/1.0">
  <eAnnotations source="http://eclipse.org/fennec/codec">
    <details key="fingerprintMode" value="FIRST_TOUCH"/>
  </eAnnotations>

A single class can still state an exception (fingerprintMode=NONE opts back out), and a caller can switch it on for one operation through the option. See 16-annotation-reference.md for the level and 02 §3.1 for how it merges.


Next: SuperType Serialization →

Released under the EPL-2.0 License. Eclipse Fennec is part of the Eclipse Foundation.