What actually converts a @RequestParam/@PathVariable String into a LocalDate, enum, or custom value object — and how do you customize or fix a conversion failure?
answer
- WebConversionService = FormattingConversionService (Converters + Formatters)
- @DateTimeFormat for dates; Converter<String,Foo> for value objects
- register via WebMvcConfigurer.addFormatters or a @Component bean
- @InitBinder + WebDataBinder = controller-scoped
- failure -> MethodArgumentTypeMismatchException -> 400; enum match is case-sensitive
basics
~20 sSpring runs the raw String through a ConversionService (a FormattingConversionService) using registered Converter and Formatter implementations. For dates you add @DateTimeFormat or a Formatter; for custom types you register a Converter<String, YourType>. Failures throw MethodArgumentTypeMismatchException -> 400.
solid answer
~40 sValues from @RequestParam/@PathVariable/@RequestHeader/@CookieValue arrive as Strings and are converted by the web layer's WebConversionService, a FormattingConversionService that holds Converters and Formatters plus the built-in ones (String->enum, String->Number, String->collection). Register a global org.springframework.core.convert.converter.Converter<String,Foo> as a bean or via WebMvcConfigurer.addFormatters, or a Formatter<Foo> for locale/pattern-aware parsing. For dates, @DateTimeFormat(iso = ISO.DATE) on the parameter drives the parse. Per-controller customization uses @InitBinder with a WebDataBinder and PropertyEditor/Formatter. On failure Spring throws MethodArgumentTypeMismatchException (a TypeMismatchException), which the default handler maps to 400 Bad Request; you can catch it in @ExceptionHandler/@ControllerAdvice to shape the error body. Prefer a global Converter for reusable value objects and @DateTimeFormat for ad-hoc date formats.
code
java · 24 lines// 1) Global value-object conversion — works for @PathVariable AND @RequestParam
@Configuration
class WebConfig implements WebMvcConfigurer {
@Override public void addFormatters(FormatterRegistry registry) {
registry.addConverter(String.class, ProductId.class,
s -> new ProductId(UUID.fromString(s)));
}
}
@RestController
class Api {
@GetMapping("/products/{id}")
Product get(@PathVariable ProductId id,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate since) {
return service.find(id, since);
}
// Shape the 400 for any bad path/query type
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
ResponseEntity<String> bad(MethodArgumentTypeMismatchException e) {
return ResponseEntity.badRequest()
.body("Invalid '" + e.getName() + "': " + e.getValue());
}
}go deeper
Knows Spring auto-converts Strings to numbers/enums/dates.
Can add @DateTimeFormat and knows a bad value gives 400.
Registers a Converter/Formatter, uses @InitBinder, and names MethodArgumentTypeMismatchException.
Understands the ConversionService architecture, conversion-before-validation ordering, and centralizes consistent error handling across path/query binding.
**The core machinery.** All the simple-input annotations (@RequestParam, @PathVariable, @RequestHeader, @CookieValue) are backed by *named-value argument resolvers*. Each pulls a raw String (or String[]) from the request and then delegates conversion to Spring's **type-conversion system**, exposed as a `ConversionService`. In Spring MVC the active instance is a `WebConversionService`, which extends `DefaultFormattingConversionService` (a `FormattingConversionService`). It contains two kinds of registered helpers: - **Converter<S,T>** (org.springframework.core.convert.converter.Converter): a stateless, generic S->T conversion, e.g. Converter<String, ProductId>. - **Formatter<T>** (org.springframework.format.Formatter): parse/print with awareness of Locale and format annotations — ideal for dates, numbers, currency. Out of the box it already handles String -> primitives/wrappers, String -> enum (by name), String -> Number, String -> collection/array (comma split), and, via registered formatters, JSR-310 types when annotated. **Dates.** A LocalDate parameter typically needs a format hint: ```java @GetMapping("/r") public X r(@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from) { ... } ``` @DateTimeFormat (with iso=... or pattern="yyyy/MM/dd") tells the Jsr310 date formatters how to parse. Without it, ISO-8601 is generally accepted for java.time types because Spring Boot registers the JSR-310 formatters, but being explicit avoids surprises and supports custom patterns. **Custom value objects — the recommended approach.** Register a Converter as a bean or via WebMvcConfigurer: ```java @Component class StringToProductIdConverter implements Converter<String, ProductId> { public ProductId convert(String s) { return new ProductId(UUID.fromString(s)); } } // or @Configuration class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToProductIdConverter()); } } ``` Now @PathVariable ProductId id and @RequestParam ProductId id both convert automatically, everywhere. **Per-controller customization — @InitBinder.** For controller-local rules, add a method annotated @InitBinder that receives a WebDataBinder and registers a custom editor/formatter (e.g. trimming strings, a controller-specific date pattern). This is narrower in scope than a global Converter. **Failure semantics.** If conversion can't produce the target type — ?from=notadate, ?status=BOGUS for an enum, ?page=abc for int — the resolver raises **MethodArgumentTypeMismatchException** (a subclass of TypeMismatchException). Spring MVC's default handling (DefaultHandlerExceptionResolver / ResponseEntityExceptionHandler) maps it to **HTTP 400 Bad Request**. You can intercept it: ```java @ControllerAdvice class Errors { @ExceptionHandler(MethodArgumentTypeMismatchException.class) ResponseEntity<ApiError> onMismatch(MethodArgumentTypeMismatchException e) { return ResponseEntity.badRequest().body(new ApiError(e.getName(), e.getValue())); } } ``` e.getName() gives the parameter name, e.getRequiredType() the target type, e.getValue() the offending input. **Enums gotcha.** String->enum matches by exact enum constant name and is case-sensitive by default. ?status=active won't match ACTIVE. Fix with a custom Converter<String, Status> that upper-cases, or accept the exact casing. **Interplay with validation.** Type conversion runs *before* Bean Validation. A malformed type fails at conversion (MethodArgumentTypeMismatchException, 400) and never reaches @Validated/@Min checks. Constraint violations on well-typed params (e.g. @Min(1) int page with page=-5) throw ConstraintViolationException (needs @Validated on the controller) — a different exception and often a different HTTP mapping. Design your error handling for both. **Design guidance:** use @DateTimeFormat for date/number formatting; register a global Converter for reusable domain value objects (ids, money, enums with aliases) so path and query bind uniformly; reserve @InitBinder for truly controller-scoped tweaks; and centralize the 400 shaping for MethodArgumentTypeMismatchException in @ControllerAdvice so clients get consistent, informative errors instead of a default stack.
- A client sends ?status=active but the enum constants are ACTIVE/INACTIVE. What happens and how do you fix it cleanly?Default String->enum conversion is by exact constant name and case-sensitive, so 'active' fails with MethodArgumentTypeMismatchException -> 400. Fix by registering a Converter<String,Status> that normalizes case (e.g. Status.valueOf(s.trim().toUpperCase())), keeping the API forgiving without touching every handler.
- What is the ordering between type conversion and Bean Validation for @RequestParam @Min(1) int page?Conversion happens first: page=abc fails conversion -> MethodArgumentTypeMismatchException (400) before any constraint runs. If it converts but violates @Min (page=-1), you get a ConstraintViolationException (requires @Validated on the controller), a distinct exception you must map separately.
saying these in an interview costs you the question
- Saying you must manually parse Strings in the controller instead of registering a Converter/Formatter.
- Believing enum binding is case-insensitive by default.
- Assuming Bean Validation runs before/instead of type conversion.
- Thinking a conversion failure yields 500 rather than 400.