Introduction
Jackson polymorphic deserialization fails when the JSON payload does not contain type information needed to determine which concrete subclass to instantiate. When a REST API accepts a base type (like Notification with subclasses EmailNotification, SmsNotification, PushNotification) but the incoming JSON does not specify which subtype it represents, Jackson cannot determine which class to deserialize into. This manifests as InvalidTypeIdException, MismatchedInputException, or deserialization into the base type with null subclass-specific fields.
Symptoms
com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class com.example.Notification]: missing type id property 'type'
at [Source: (String)"{"subject":"Hello","body":"World"}"; line: 1, column: 35]Or:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of com.example.Notification (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)Or silent failure where subclass-specific fields are null:
```json // Input {"subject": "Hello", "body": "World", "phoneNumber": "+1234567890"}
// Deserialized as EmailNotification (default) - phoneNumber is null ```
Common Causes
- No @JsonTypeInfo on base class: Jackson has no instruction on how to determine the subtype
- Type property missing in JSON: The JSON does not include the discriminator field (e.g.,
"type": "email") - Type property name mismatch:
@JsonTypeInfo(use = Id.NAME, property = "notificationType")but JSON has"type" - Subtype not registered:
@JsonSubTypesdoes not include all possible subclasses - Default typing enabled globally:
ObjectMapper.enableDefaultTyping()causes security issues and unexpected behavior - Using interface as target type without type info: Jackson cannot instantiate interfaces
Step-by-Step Fix
Step 1: Configure @JsonTypeInfo and @JsonSubTypes on the base class
```java @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type" ) @JsonSubTypes({ @JsonSubTypes.Type(value = EmailNotification.class, name = "email"), @JsonSubTypes.Type(value = SmsNotification.class, name = "sms"), @JsonSubTypes.Type(value = PushNotification.class, name = "push") }) public abstract class Notification { private String subject; private String body; }
public class EmailNotification extends Notification { private String recipientEmail; private List<String> cc; }
public class SmsNotification extends Notification { private String phoneNumber; } ```
JSON input:
{
"type": "sms",
"subject": "Alert",
"body": "Server is down",
"phoneNumber": "+1234567890"
}Step 2: Use existing property as type discriminator
If the JSON already has a field that can serve as the discriminator:
```java @JsonTypeInfo( use = JsonTypeInfo.Id.DEDUCTION, defaultImpl = EmailNotification.class ) @JsonSubTypes({ @JsonSubTypes.Type(EmailNotification.class), @JsonSubTypes.Type(SmsNotification.class) }) public abstract class Notification { private String subject; private String body; }
// EmailNotification has "recipientEmail" - if present, Jackson deduces EmailNotification public class EmailNotification extends Notification { private String recipientEmail; }
// SmsNotification has "phoneNumber" - if present, Jackson deduces SmsNotification public class SmsNotification extends Notification { private String phoneNumber; } ```
Step 3: Handle missing type property with a default
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "type",
visible = true
)
@JsonSubTypes({
@JsonSubTypes.Type(value = EmailNotification.class, name = "email"),
@JsonSubTypes.Type(value = SmsNotification.class, name = "sms")
})
public abstract class Notification {
private String type; // The discriminator field
private String subject;
private String body;
}Prevention
- Always use
@JsonTypeInfoand@JsonSubTypeson base classes for polymorphic deserialization - Add a unit test for each subtype to verify round-trip serialization/deserialization
- Use
defaultImplto handle unknown type values gracefully - Avoid
ObjectMapper.enableDefaultTyping()-- it is a security risk (CVE-2017-7525) - Include the type discriminator field in your OpenAPI specification
- Use
@JsonIgnoreProperties(ignoreUnknown = true)to handle extra fields from newer clients