Skip to content

Deserializer Discovery 2.x

Tatu Saloranta edited this page Jan 30, 2024 · 19 revisions

Discovering POJO Deserializers in Jackson 2.x

This page describes the process of discovering JsonDeserializers for POJO (aka Bean) types in Jackson 2.x It serves as the background/context for low-level POJO Property Introspection document (TO BE WRITTEN).

General

JsonDeserializers are needed for reading JSON (and other supported formats) from JsonParser and constructing desired Java Objects. Discovery process is initiated by 3 entities:

  1. ObjectMapper to locate deserializer to use for target type indicated for readValue() method (and readValues(), convertValue())
  2. ObjectReader (similar to ObjectMapper)
  3. Deserializers themselves, to locate "child deserializers": for example when deserializing Lists, deserializer for elements contained is separate from deserializer for List itself (and similarly for other structured types like java.util.Maps, Arrays, POJOs)

Discovery process for these cases is almost identical (and in fact, (1) and (2) are identical), differing only in that for (3), contextualization (via method DeserializationContext._handleSecondaryContextualization(...)) passes referring property definition (BeanProperty) whereas one does not exist for "root" values.

Deserializer Contextualization vs Resolution

Two terms for deserializers (as defined by ResolvableDeserializer and ContextualDeserializer) are:

  1. Resolution is needed to handle cyclic dependencies between deserialializers: this is mostly relevant for BeanDeserializer.
  2. Contextualization is needed to give initially context-free (only based on Type) deserializers access to annotations added to property (that is, on Field and/or Method and/or Constructor parameter).

Understanding this processing is not strictly necessary to understand call flow, but is useful to know since these are referenced in couple of places.

High-level call sequence

ObjectMapper to DeserializationContext

The first step to trigger discovery is ObjectMapper (and other entities mentioned earlier) calling one of:

  1. Explicit findXxxDeserializer() method such as findRootValueDeserializer()
  2. One of readXxxValue() convenience methods like readValue() which will need to call one of findXxxDeserializer() methods

Either way method like findRootValueDeserializer() is called; and this is the call path we will focus on.

DeserializationContext to DeserializerCache

Big part of actual discovery is handled by DeserializerCache: the main entry point is method findRootValueDeserializer() (or one of alternatives). It will not only handle caching of constructed and resolved (but not contextualized) deserializers but also:

  1. Mapping of abstract Java types (as well as Map and Collection types) into concrete (for abstract) and default (Map, Collection) implementation types
  2. Introspecting BeanDescription (of type BasicBeanDescription) for type indicated
  3. Handling annotations on Java types (Classes) that directly indicate JsonDeserializer to use (@JsonDeserialize(using = IMPL_CLASS))
  4. Refinement of actual type to use (@JsonDeserialize(as = CONCRETE_TYPE)); and re-introspecting BeanDescription for refined type
  5. Use of "converting" deserializers indicated by annotation (and directly constructing StdDelegatingDeserializer as needed)
  6. Calling appropriate method of configured DeserializerFactory to actually construct JsonDeserializer to use
  7. Resolution of ResolveDeserializers, to avoid StackOverflowError for cyclic types (put another way: allowing use of cyclic type definitions)

and last but not least:

  • Calling type-specific method in DeserializerFactory as necessary to actually construct deserializer.

DeserializerCache to DeserializerFactory

With given target JavaType and introspected BeanDescription, DeserializerCache will call one of following methods of DeserializerFactory (selected in following order)

  1. createEnumDeserializer() (if JavaType.isEnumType() returns true)
  2. createArrayDeserializer() (JavaType.isArrayType())
  3. createMapDeserializer()/createMapLikeDeserializer() ("Map-like" types refer to Scala (f.ex) types that act like java.util.Map but do not implement that interface)
  4. createCollectionDeserializer()/createCollectionLikeDeserializer() ("Collection-like" types similarly refer to Scala's collection types that work similar to java.util.Collection but do not implement it)
  5. createReferenceDeserializer() for types like AtomicReference<> (JDK), Optional (JDK 8+, Guava), Option (Scala)
  6. createTreeDeserializer() for JsonNode (and subtypes)
  7. createBeanDeserializer() if none of above matches

We will focus on the last case, in which general POJO (aka "Bean") deserializer is constructed.

DeserializerFactory

Actual DeserializerFactory used is BeanDeserializerFactory, which extends BasicDeserializerFactory (which implements DeserializerFactory except for createBeanDeserializer()).

Here we will focus on BeanDeserializerFactory.createBeanDeserializer() implementation: it will

  1. Look for custom JsonDeserializer registration (by Modules) (which are registered as Deserializers implementation): if any register, call Deserializers.findBeanDeserializer()
  2. If no custom deserializer found, and target type is Throwable (or subtype), call buildThrowableDeserializer() to construct specialized variant of BeanDeserializer
  3. Otherwise if target type is abstract, call materializeAbstractType() which may provide deserializer (typically by one of registered modules such as MrBeanModule)
  4. If no deserializer found yet, see if one of "standard" deserializers -- ones handling recognized JDK types (other than Enum/Array/Collection(Like)/Map(Like) types that are handled by different factory methods) -- and if so, construct one

And ultimately if nothing is found, call actual buildBeanDeserializer() method with context, type and BeanDescription.

Detailed call path for constructing BeanDeserializer

High-level description has many branches off for different cases other than producing a standard POJO Deserializer (named BeanDeserializer for historic reasons). But let's focus on specific call path/sequence that results in a new BeanDeserializer

Entry: DeserializationContext.findRootValueDeserializer()

So when call is made like:

ObjectMapper mapper = new ObjectMapper();
MyValue value = mapper.readValue(jsonInput, MyValue.class);

we will get a call to DeserializationContext method findRootValueDeserializer(). This will call DeserializerCache.findValueDeserializer() which will try to locate a deserializer already created, and if failing to do so (which is the case the first time it gets called like above), proceed with discovery and construction of needed deserializer. After deserializer has been returned, it will be contextualized as necessary (by a call to ContextualDeserializer.createContextual() -- we will not be going through contextualization process here).

DeserializerCache.findValueDeserializer()

  1. First thing done in this method is to look for already cached deserializers (see _findCachedDeserializer(); nothing special here).
  2. Assuming no cached deserializer is found, method _createAndCacheValueDeserializer() is called
    • _createAndCacheValueDeserializer() handles synchronization aspects (by keeping track of in-process deserializers in its _incompleteDeserializers Map), but it calls _createAndCache2() for further processing.
  3. _createAndCache2() will:
    • delegate actual construction further to _createDeserializer() (which is covered in next section)
    • trigger deserializer (dependency) resolution if deserializer implements ResolvableDeserializer (in which case its resolve() method is called)
    • add deserializer into _cachedDeserializers if (but only if!) it is eligible for caching

DeserializerCache._createDeserializer()

This is where most of the magic is found:

  1. First, abstract and Collection and Map types are mapped (to "concrete" and "default" types, respectively)
  2. Once we have likely type to use, BeanDescription is obtained via Property Introspection through DeserializationConfig.introspect(JavaType) method (which in turn calls ClassIntrospector.forDeserialization() method)
    • !!! This is where Property Introspection occurs !!!
  3. Once we have BeanDescription, we check if a Class-Annotated deserializer is found (by call to findDeserializerFromAnnotation()) and if so, create instance and return it. Check is made by a call to
    • Annotation checked is @JsonDeserialize(using = DESER_IMPL.class), but lookup is via configured AnnotationIntrospector

--- TO BE COMPLETED ---

Clone this wiki locally