Introduction
Jackson's polymorphic deserialization uses type identifiers in JSON to determine which Java class to instantiate. When the type ID in the JSON does not match any registered subtype, Jackson throws InvalidTypeIdException: Could not resolve type id 'unknown_type' as a subtype of known types. This commonly occurs when API clients send new types that the server does not yet know about, when type IDs are misspelled, or when subtype registration is incomplete. Without proper handling, these errors are fatal and block the entire deserialization process.
Symptoms
com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'email' as a subtype of `com.example.Notification`: known type ids = [sms, push]
at [Source: (String)"{"type":"email","content":"Hello"}"; line: 1, column: 9]Or security-related:
com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of type 'java.lang.Object': missing type id property '@class'Common Causes
- Subtype not registered: @JsonSubTypes does not include the new type
- Type ID mismatch: JSON has "email" but annotation has "EmailNotification"
- API version mismatch: Client sends new type, server not updated yet
- Default handler not configured: No fallback for unknown type IDs
- Using default typing: Enables deserialization of arbitrary types (security risk)
- Type property renamed: JSON field name differs from @JsonTypeInfo property
Step-by-Step Fix
Step 1: Configure polymorphic type handling
```java @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type" ) @JsonSubTypes({ @JsonSubTypes.Type(value = SmsNotification.class, name = "sms"), @JsonSubTypes.Type(value = PushNotification.class, name = "push"), @JsonSubTypes.Type(value = EmailNotification.class, name = "email"), }) public abstract class Notification { private String content; // getters and setters }
public class EmailNotification extends Notification { private String to; private String subject; // getters and setters }
// JSON input: {"type":"email","to":"user@example.com","subject":"Hello","content":"Hi"} // Correctly deserializes to EmailNotification ```
Step 2: Handle unknown type IDs gracefully
```java @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", defaultImpl = UnknownNotification.class // Fallback for unknown types ) @JsonSubTypes({ @JsonSubTypes.Type(value = SmsNotification.class, name = "sms"), @JsonSubTypes.Type(value = PushNotification.class, name = "push"), }) public abstract class Notification { }
public class UnknownNotification extends Notification { @JsonAnySetter private Map<String, Object> properties = new HashMap<>();
// Captures all fields from unknown notification types public Map<String, Object> getProperties() { return properties; } } ```
Step 3: Secure polymorphic deserialization
```java // NEVER use default typing with Object.class // WRONG - allows deserialization of any class (security vulnerability): // objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// CORRECT - restrict to known subtypes: ObjectMapper mapper = new ObjectMapper(); mapper.registerSubtypes( new NamedType(SmsNotification.class, "sms"), new NamedType(PushNotification.class, "push"), new NamedType(EmailNotification.class, "email") );
// Block unknown types entirely for security mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); ```
Prevention
- Always register all subtypes with @JsonSubTypes or registerSubtypes()
- Use a defaultImpl for forward compatibility with new types
- Never enable default typing with Object.class -- it allows arbitrary class instantiation
- Use JsonTypeInfo.Id.NAME (not CLASS or MINIMAL_CLASS) to avoid exposing class names
- Add unknown type handling to log warnings about unrecognized types
- Test deserialization with unknown type IDs to verify graceful degradation
- Keep type ID names simple and consistent (lowercase, not class names)