Type-Safe Conversions Spring Boot 4.0.2
Register custom String↔Type 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
String↔Type 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.