Type-Safe Conversions Spring Boot 4.0.2

Register custom StringType two-way conversions that report structured errors through RSpond status handlers. Works seamlessly with Spring MVC path variables.

pom.xml
<dependency> <groupId>org.rspond</groupId> <artifactId>rspond-spring-conversion</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency>
UserIdConversionApplication.java
package org.rspond.examples.springboot.conversion; import org.rspond.conversion.Conversion; import org.rspond.conversion.registry.ConversionRegistry; import org.rspond.conversion.registry.ConversionRegistryApiTrait; import org.rspond.conversions.string.StringConversionApiTraits; import org.rspond.converters.java.JavaConvertersApiTrait; import org.rspond.spring.conversion.RSpondSpringConversionApiTrait; import org.rspond.spring.conversion.RSpondSpringConversionErrorAdvice; import org.rspond.spring.conversion.RSpondSpringConversionWebMvcConfigurer; import org.rspond.status.StatusApiTraits; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController @RequestMapping("/api") @Import(RSpondSpringConversionErrorAdvice.class) public class UserIdConversionApplication implements StatusApiTraits, JavaConvertersApiTrait, ConversionRegistryApiTrait, StringConversionApiTraits, RSpondSpringConversionApiTrait { public record UserId(long value) {} public static void main(String[] args) { SpringApplication.run(UserIdConversionApplication.class, args); } @GetMapping("/users/{id}") public ResponseEntity<?> user(@PathVariable UserId id) { return ResponseEntity.ok(id.value()); } @Bean ConversionRegistry conversionRegistry() { return newConversionRegistry().withConversion(userIdConversion()).create(); } @Bean RSpondSpringConversionWebMvcConfigurer springConversionWebMvcConfigurer(ConversionRegistry registry) { return springConversionWebMvcConfigurer(this::statusList, registry); } Conversion<String, UserId> userIdConversion() { return newStringConversion(type(UserId.class), (handler, text) -> { var id = javaLongConverter().convert(handler, text); return id == null || handler.hasErrors() ? null : new UserId(id); }).create(); } }

Key Concepts

ConversionRegistry
Central registry for type-safe conversions
@Import(RSpondSpringConversionErrorAdvice.class)
Spring MVC error advice for conversion failures via @ControllerAdvice
newStringConversion()
Builder for two-way StringType conversions with status reporting

Try It

# Valid user ID curl localhost:8080/api/users/42 # → 42 # Invalid user ID — structured error response curl localhost:8080/api/users/abc # → 400 Bad Request with structured error

Validation Spring Boot 4.0.2

Validators that integrate with RSpond structured error handling. Validate strings, objects, integers—anything. Use them in your REST API, data tier, pipelines, or anywhere in your backend.

pom.xml
<dependency> <groupId>org.rspond</groupId> <artifactId>rspond-validators-java</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency>
ZipCodeValidationApplication.java
package org.rspond.examples.springboot.validation; import org.rspond.status.StatusApiTraits; import org.rspond.validation.Validator; import org.rspond.validators.java.JavaValidatorsApiTrait; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.regex.Pattern; import static org.springframework.http.HttpStatus.BAD_REQUEST; @SpringBootApplication @RestController @RequestMapping("/api") public class ZipCodeValidationApplication implements StatusApiTraits, JavaValidatorsApiTrait { private final Validator<String> requireValidUsZipCode = requireMatches(Pattern.compile("^\\d{5}(-\\d{4})?$")); public static void main(String[] args) { SpringApplication.run(ZipCodeValidationApplication.class, args); } @GetMapping("/zip/{zip}") public ResponseEntity<?> validateZip(@PathVariable String zip) { var captured = statusList(); requireValidUsZipCode.validate(captured, zip); if (captured.hasErrors()) { return ResponseEntity.status(BAD_REQUEST).body(captured.toString()); } return ResponseEntity.ok().body("OK"); } }

Key Concepts

Validator<T>
Composable validation interface—works with any type, not just String
requireMatches(Pattern)
One built-in validator; others validate objects, integers, addresses, and more
statusList()
Accumulates validation errors as structured statuses
Backend reusability
Same validators work in REST controllers, data tiers, pipelines—anywhere in your backend, with or without Spring

Try It

# Valid US zip code curl localhost:8080/api/zip/90210 # → "OK" # Invalid zip code — structured validation errors curl localhost:8080/api/zip/hello # → 400 with structured validation errors

Uniform Resources Spring Boot 4.0.2

Resource and folder abstraction for file systems, classpaths, and cloud storage. Read content, metadata, and structured folder hierarchies through a single, consistent API.

pom.xml
<dependency>
  <groupId>org.rspond</groupId>
  <artifactId>rspond-resource</artifactId>
  <version>1.0.0-SNAPSHOT</version>
</dependency>
ResourceApplication.java
package org.rspond.examples.springboot.resources;

import org.rspond.io.InputReader;
import org.rspond.resource.store.node.resource.ResourceApiTrait;
import org.rspond.values.bytes.Bytes;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.springframework.http.HttpStatus.BAD_REQUEST;

@SpringBootApplication
@RestController
@RequestMapping("/api")
public class ResourceApplication implements ResourceApiTrait
{
    public static void main(String[] args) {
        SpringApplication.run(ResourceApplication.class, args);
    }

    @GetMapping("/resource/{number}")
    public ResponseEntity<?> resourceByNumber(@PathVariable String number) {
        var resource = resource("classpath:///org/rspond/examples/springboot/resources/" + number + ".txt");
        if (resource.exists()) {
            var size = resource.metadata().size().map(Bytes::toString).orElse("? bytes");
            return ResponseEntity.ok().body(size + ": " + resource.withReader(InputReader::readAllText));
        }
        return ResponseEntity.status(BAD_REQUEST).body("Couldn't find resource: " + number);
    }
}

Key Concepts

resource()
Factory method creating a Resource from any URI
withReader(InputReader::readAllText)
Streaming content access with automatic lifecycle management
metadata().size()
Uniform metadata across all storage backends

Where This Is Going

Beyond individual resources, RSpond provides resource folders with full recursion support. Using ClassGraph under the hood, you can recursively search your classpath as a folder hierarchy—making resource discovery as natural as browsing a file system. Cloud storage backends support the same folder abstraction.

Try It

# Existing resource curl localhost:8080/api/resource/1 # → "42 bytes: [content of 1.txt]" # Missing resource curl localhost:8080/api/resource/999 # → 400 "Couldn't find resource: 999"

Before & After

Real, runnable examples showing what typical Java code looks like before and after RSpond. The “before” code works fine—it’s just verbose, repetitive, and stops at the first error. RSpond eliminates the ceremony and collects all the errors.

Example: Settings Parser

A properties file has 3 bad values. The traditional approach: 9 try-catch blocks, 141 lines, and the first error kills the whole parse. With RSpond: typed converters, ~50 lines, and all errors collected.

settings.properties — input file (3 bad values highlighted)
app.port=xyz app.maxThreads=200 app.timeoutMillis=bad-number app.debug=maybe app.featureXEnabled=true app.dataDir=/tmp/data app.logFile=/tmp/app.log app.logLevel=INFO app.handlerClass=java.lang.String

The parse() method — side by side

Before141 LINES
public Settings parse(Properties props) { Integer port; try { port = Integer.parseInt( required(props, "app.port")); } catch (Exception e) { throw new RuntimeException( "Invalid: app.port", e); } Integer maxThreads; try { maxThreads = Integer.parseInt( required(props, "app.maxThreads")); } catch (Exception e) { throw new RuntimeException( "Invalid: app.maxThreads", e); } Long timeoutMillis; try { timeoutMillis = Long.parseLong( required(props, "app.timeoutMillis")); } catch (Exception e) { throw new RuntimeException( "Invalid: app.timeoutMillis", e); } Boolean debug; try { debug = parseBooleanStrict( required(props, "app.debug")); } catch (Exception e) { throw new RuntimeException( "Invalid: app.debug", e); } // ... 5 more identical try-catch blocks // for featureX, dataDir, logFile, // logLevel, handlerClass return new Settings(port, maxThreads, timeoutMillis, debug, /* ... */); } // + parseBooleanStrict() helper // + required() helper // + loadProperties() helper
After v1 — Typed Converters51 LINES
public Settings parse(Properties props) { var port = integerConverter() .convert(this, props.getProperty("app.port")); var maxThreads = integerConverter() .convert(this, props.getProperty("app.maxThreads")); var timeoutMs = longConverter() .convert(this, props.getProperty("app.timeoutMillis")); var debug = booleanConverter() .convert(this, props.getProperty("app.debug")); var featureX = booleanConverter() .convert(this, props.getProperty("app.featureXEnabled")); var dataDir = pathConverter() .convert(this, props.getProperty("app.dataDir")); var logFile = pathConverter() .convert(this, props.getProperty("app.logFile")); var logLevel = enumConverter(LogLevel.class) .convert(this, props.getProperty("app.logLevel")); var handler = classConverter() .convert(this, props.getProperty("app.handlerClass")); if (hasErrors()) { reportError("Invalid settings"); return null; } return new Settings(port, maxThreads, timeoutMs, debug, /* ... */); } // No helpers needed. // No try-catch anywhere. // All errors collected.

Terminal output — side by side

Before — first error stops everything
Failed to parse settings: RuntimeException: Invalid setting: app.port (expected integer) at ...BeforeSettingsParser .parse(line 36) at ...BeforeSettingsParser .main(line 12) Caused by: NumberFormatException: For input string: "xyz" at java.base/Integer.parseInt ... 1 more // The 2 other bad values? // Never reached.
After — all 3 errors collected
2026.02.14 | Error | Invalid Integer: xyz 2026.02.14 | Error | Invalid Long: bad-number 2026.02.14 | Error | Invalid boolean: maybe 2026.02.14 | Error | Invalid settings // All errors found in one pass. // Structured, not stack traces.

After v2 — ConvertingVariableMap 56 LINES

About the same length as v1, but you don’t need to find the converters to write the code. Every conversion is just properties.convert(handler, "key", Type.class) — the variable map uses the conversion registry to locate the converter for the target type automatically.

AfterSettingsParser2.java — parse() method
public Settings parse(StatusHandler handler, ConvertingVariableMap<String> properties) { var port = properties.convert(handler, "app.port", Integer.class); var maxThreads = properties.convert(handler, "app.maxThreads", Integer.class); var timeoutMs = properties.convert(handler, "app.timeoutMillis", Long.class); var debug = properties.convert(handler, "app.debug", Boolean.class); var featureX = properties.convert(handler, "app.featureXEnabled", Boolean.class); var dataDir = properties.convert(handler, "app.dataDir", Path.class); var logFile = properties.convert(handler, "app.logFile", Path.class); var logLevel = properties.convert(handler, "app.logLevel", LogLevel.class); var handler = properties.convert(handler, "app.handlerClass", Class.class); if (hasErrors()) { reportError("Invalid settings"); return null; } return new Settings(port, maxThreads, timeoutMs, debug, featureX, dataDir, logFile, logLevel, handler); }

Example: Integer Parsing

The traditional approach requires separate methods for “throw on error” vs. “return null on error” plus a hand-rolled helper. With RSpond, one method handles both—the caller chooses the error strategy with a StatusHandler.

Before3 METHODS + HELPER
public class BeforeParseIntegerTest { @Test public void testParseIntegerOrThrow() { assertEquals(2, addOrThrow("1", "1")); assertThrows(RuntimeException.class, () -> addOrThrow("1", "xyz")); } @Test public void testParseIntegerOrNull() { assertEquals(2, addOrNull("1", "1")); assertNull(addOrNull("1", "xyz")); } /** Hand-rolled helper */ private Integer parseIntegerOrNull(String text) { try { return Integer.parseInt(text); } catch (NumberFormatException e) { return null; } } private Integer addOrNull( String a, String b) { var x = parseIntegerOrNull(a); var y = parseIntegerOrNull(b); if (x != null && y != null) return x + y; return null; } private Integer addOrThrow( String a, String b) { return Integer.parseInt(a) + Integer.parseInt(b); } }
After1 METHOD
public class AfterParseIntegerTest extends TestBase implements JavaConvertersApiTrait { @Test public void testParseIntegerOrThrow() { assertEquals(2, add(throwOnError(), "1", "1")); assertThrows(RuntimeException.class, () -> add(throwOnError(), "1", "xyz")); } @Test public void testParseIntegerOrNull() { assertEquals(2, add(returnOnError(), "1", "1")); assertNull(add(returnOnError(), "1", "xyz")); } // One method — caller picks strategy private Integer add( StatusHandler handler, String a, String b) { var x = integerConverter() .convert(handler, a); var y = integerConverter() .convert(handler, b); if (x != null && y != null) return x + y; return null; } }

Key Concepts

StatusHandler
Passed to operations that might fail. The caller chooses the strategy: throwOnError(), returnOnError(), statusList(), etc.
Typed Converters
integerConverter(), booleanConverter(), pathConverter(), enumConverter(), classConverter() — each reports errors through the handler instead of throwing
ConvertingVariableMap
properties.convert(this, "key", Type.class) — auto-locates the right converter from the registry based on the target type
Error Accumulation
All errors are collected, not just the first. Check with hasErrors() after all conversions to get the full picture.

Ready to integrate RSpond?

Start with the quickstart guide or join our design partner program for hands-on support integrating RSpond into your Spring Boot applications.

Quickstart → Become a Partner