skip to content

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?

level: principalimportance: should knowfreq 35%

answer

  1. WebConversionService = FormattingConversionService (Converters + Formatters)
  2. @DateTimeFormat for dates; Converter<String,Foo> for value objects
  3. register via WebMvcConfigurer.addFormatters or a @Component bean
  4. @InitBinder + WebDataBinder = controller-scoped
  5. failure -> MethodArgumentTypeMismatchException -> 400; enum match is case-sensitive

basics

~20 s

Spring 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 s

Values 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
java
// 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

for a junior

Knows Spring auto-converts Strings to numbers/enums/dates.

for a middle

Can add @DateTimeFormat and knows a bad value gives 400.

for a senior

Registers a Converter/Formatter, uses @InitBinder, and names MethodArgumentTypeMismatchException.

for a principal

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.

context