Compare Converter, ConverterFactory, and GenericConverter. When would you choose each?
answer
- Converter = 1:1
- ConverterFactory = 1→family of subtypes (String→Enum/Number)
- GenericConverter = many pairs + TypeDescriptor context
- ConditionalGenericConverter adds matches()
- source never null; keep stateless
basics
~20 sConverter<S,T> converts one source type to one target type. ConverterFactory<S,R> converts one source to a whole family of subtypes (e.g. String→any Enum). GenericConverter is the most powerful: it can handle multiple type pairs and see field context/annotations.
solid answer
~40 sAll three plug into the same ConverterRegistry. Converter<S,T> is the simplest: one source type to one target type, e.g. String→LocalDate. ConverterFactory<S,R> produces converters for a range of target subtypes sharing a base — the classic examples are String→Number (Integer, Long, BigDecimal…) and String→Enum; you implement getConverter(Class<T> targetType). GenericConverter is the low-level, most flexible option: getConvertibleTypes() returns a Set of ConvertiblePair, and convert(Object, TypeDescriptor source, TypeDescriptor target) gives you full TypeDescriptor context — generics, annotations, the actual field. ConditionalGenericConverter (and ConditionalConverter) adds matches() so a converter can opt out for specific descriptors. Choose Converter for simple 1:1, ConverterFactory for a type hierarchy, and GenericConverter when you need collections, arrays, or annotation/context-aware behavior. Converters must be null-safe-by-omission (they never receive null) and thread-safe.
code
java · 24 lines// 1:1 Converter
public class StringToIsbnConverter implements Converter<String, Isbn> {
@Override public Isbn convert(String source) { return Isbn.parse(source.trim()); }
}
// ConverterFactory: String -> any enum subtype
public class StringToEnumFactory implements ConverterFactory<String, Enum> {
@Override public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return source -> (T) Enum.valueOf(targetType, source.trim());
}
}
// ConditionalGenericConverter: only matches fields annotated @MaskedId
public class MaskedIdConverter implements ConditionalGenericConverter {
@Override public Set<ConvertiblePair> getConvertibleTypes() {
return Set.of(new ConvertiblePair(String.class, Long.class));
}
@Override public boolean matches(TypeDescriptor s, TypeDescriptor t) {
return t.hasAnnotation(MaskedId.class);
}
@Override public Object convert(Object src, TypeDescriptor s, TypeDescriptor t) {
return IdMasker.unmask((String) src);
}
}go deeper
Know Converter<S,T> exists and does 1:1 conversion.
Correctly contrast all three and give the String→Enum/Number ConverterFactory examples.
Discuss TypeDescriptor context, ConditionalGenericConverter.matches(), and precedence/hierarchy search.
Advise on API design: when the extra power of GenericConverter is justified vs. over-engineering; registration surface across core/MVC/Boot.
Spring's conversion SPI (`org.springframework.core.convert.converter`) offers three implementer-facing interfaces, all registered into a `ConverterRegistry` (which `GenericConversionService` implements). ## 1. `Converter<S, T>` ```java public interface Converter<S, T> { T convert(S source); } ``` The simplest: exactly one source type `S` to one target type `T`. Example: `Converter<String, LocalDate>`. The framework guarantees `source` is **never null** (null sources short-circuit before reaching you). Use it for straightforward 1:1 conversions. ## 2. `ConverterFactory<S, R>` ```java public interface ConverterFactory<S, R> { <T extends R> Converter<S, T> getConverter(Class<T> targetType); } ``` For converting one source type into a **whole family of target subtypes** that share a common super-type `R`. The registry, when asked for `String→SomeEnum`, calls `getConverter(SomeEnum.class)` and caches the returned `Converter`. Spring's own `StringToNumberConverterFactory` (`R = Number`, produces Integer/Long/BigDecimal/…) and `StringToEnumConverterFactory` (`R = Enum`) are the canonical examples. Use it when a single algorithm parameterized by target class covers many types. ## 3. `GenericConverter` ```java public interface GenericConverter { Set<ConvertiblePair> getConvertibleTypes(); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); } ``` The most powerful and lowest-level. `getConvertibleTypes()` declares one or more `ConvertiblePair(sourceType, targetType)` it supports, so a single converter can handle **many pairs** (e.g. all Collection↔Array conversions). Crucially, `convert` receives full `TypeDescriptor`s, giving access to **generic element types, annotations on the field, and the member context** — impossible with the simpler interfaces. Spring implements collection, array, map, and stream conversions as `GenericConverter`s. ### `ConditionalConverter` / `ConditionalGenericConverter` `ConditionalGenericConverter` extends `GenericConverter` with: ```java boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); ``` This lets a converter be registered for a broad pair but **dynamically opt out** for specific descriptors — e.g. an ID-string converter that only matches when the target field carries a particular annotation. ## Registration All three go into the registry: `converterRegistry.addConverter(...)`, `addConverterFactory(...)`, or `addConverter(GenericConverter)`. In Spring MVC you register via `WebMvcConfigurer#addFormatters(FormatterRegistry)`; in Spring Boot, just declare them as beans and they're detected. ## Selection rules & precedence When resolving a conversion, `GenericConversionService` searches the target type's class hierarchy and interfaces, honoring `matches()` on conditional converters. A more specific `ConvertiblePair` beats a broader one. If two apply, the first matching in the hierarchy search wins — so avoid overlapping registrations. ## Gotchas - **Never return/receive null carelessly**: source is never null; returning null from a `Converter` means "converted to null", which for a primitive target causes a failure. - **Thread-safety**: converters are singletons shared across threads — keep them stateless. - **Don't throw checked exceptions**: wrap failures; Spring surfaces them as `ConversionFailedException`. - Reaching for `GenericConverter` when a plain `Converter` suffices is over-engineering; reaching for `Converter` when you need annotation context forces ugly workarounds.
- Why can't a plain Converter<String, Long> read an annotation on the target field?Converter.convert only receives the raw source value — no TypeDescriptor. Only GenericConverter (and its conditional variant) receive source/target TypeDescriptors that expose annotations, generics, and the member context.
- What does ConditionalGenericConverter's matches() buy you over GenericConverter?It lets one converter register for a broad type pair but selectively participate only when the descriptors satisfy a runtime condition (e.g. a specific annotation or generic parameter), instead of unconditionally handling every instance of that pair.
saying these in an interview costs you the question
- Claiming Converter can access field annotations or generics
- Saying ConverterFactory converts a family of SOURCE types (it's target subtypes)
- Thinking converters may hold per-call mutable state
- Believing a Converter receives null sources it must guard against