Core Concepts
Value Conversions
Given an HTTP request...
GET /test?number=123
GET /test?number=123
...and a corresponding Resource Method...
@GET("/test")
public String test(@QueryParameter Integer number) {
return String.format("Hello %d", number);
}
@GET("/test")
public String test(@QueryParameter Integer number) {
return String.format("Hello %d", number);
}
...how does Soklet convert the query parameter number to an instance of java.lang.Integer?
The answer: it consults a ValueConverterRegistry which contains a set of ValueConverter<F,T>, each instance of which knows how to convert a "from" type F to a "to" type T.
Value conversions will be automatically applied to Resource Method parameters decorated with any of these annotations:
@QueryParameter@PathParameter@RequestCookie@RequestHeader@FormParameter@Multipart@RequestBody(only by default; you likely want to perform custom parsing usingRequestBodyMarshalerinstead)
Default Conversions
Via ValueConverterRegistry, Soklet provides out-of-the-box support for the following conversions to JDK types:
String⇒Integer(and primitiveint)String⇒Long(and primitivelong)String⇒Double(and primitivedouble)String⇒Float(and primitivefloat)String⇒Byte(and primitivebyte)String⇒Short(and primitiveshort)String⇒Character(and primitivechar)String⇒Boolean(and primitiveboolean)String⇒BigIntegerString⇒BigDecimalString⇒NumberString⇒UUIDString⇒Date(ISO 8601, e.g.2025-10-02T14:05:10.973318Zor millis since Epoch, e.g.1708878162881)String⇒Instant(ISO 8601, e.g.2025-10-02T14:05:10.973318Zor millis since Epoch, e.g.1708878162881)String⇒LocalDate(ISO 8601, e.g.2024-02-25)String⇒LocalTime(ISO 8601, e.g.23:15or23:15:10)String⇒LocalDateTime(ISO 8601, e.g.2024-02-25T10:15:30)String⇒TimeZone(Olson TZ identifier, e.g.America/New_York)String⇒ZoneId(Olson TZ identifier, e.g.America/New_York)String⇒Locale(IETF BCP 47 language tag, e.g.pt-BR)String⇒Currency(ISO 4217 currency code, e.g.BRL)
The set of default converters is progammatically accessible via ValueConverters::defaultValueConverters.
There are two other quality-of-life features provided by ValueConverterRegistry:
A special reflexive
ValueConverter<F,T>is automatically used for scenarios in whichFis equal toT.For any
StringtoEnum<E extends Enum<E>>conversions, aValueConverter<F,T>is automatically generated if necessary and cached off. This is almost always what you want.
For example, given this declaration:
enum Testing {
ONE,
TWO,
THREE
}
enum Testing {
ONE,
TWO,
THREE
}
This code will "just work" without any additional setup:
@GET("/example")
public String example(@QueryParameter Testing testing) {
return String.format("Hello %s", testing.name());
}
@GET("/example")
public String example(@QueryParameter Testing testing) {
return String.format("Hello %s", testing.name());
}
At runtime, Soklet will automatically generate and cache a ValueConverter<F,T> that knows how to convert a String to Testing.ONE, Testing.TWO, or Testing.THREE so you do not have to manually create and register a custom converter.
Custom Conversions
Suppose we want to accept a typed JWT, which is comprised of 3 Base64-URL-encoded segments separated by a . character, e.g. a.b.c. Let's create a record to represent it:
public record Jwt(
String header,
String payload,
String signature
) {}
public record Jwt(
String header,
String payload,
String signature
) {}
Next, we need to make a ValueConverter<String, Jwt>. We could directly implement the interface, but using Soklet's abstract convenience class FromStringValueConverter<T> takes care of the boilerplate and lets us focus solely on the conversion code:
ValueConverter<String, Jwt> jwtVc = new FromStringValueConverter<>() {
@Nonnull
public Optional<Jwt> performConversion(@Nullable String from) throws Exception {
if(from == null)
return Optional.empty();
// JWT is of the form "a.b.c", break it into pieces
String[] components = from.split("\\.");
Jwt jwt = new Jwt(components[0], components[1], components[2]);
return Optional.of(jwt);
}
};
ValueConverter<String, Jwt> jwtVc = new FromStringValueConverter<>() {
@Nonnull
public Optional<Jwt> performConversion(@Nullable String from) throws Exception {
if(from == null)
return Optional.empty();
// JWT is of the form "a.b.c", break it into pieces
String[] components = from.split("\\.");
Jwt jwt = new Jwt(components[0], components[1], components[2]);
return Optional.of(jwt);
}
};
Soklet now needs to know about our custom ValueConverter<String, Jwt>. We create a ValueConverterRegistry - which already is prefilled with default Value Converters - and provide it with our additional custom converter:
// Create a ValueConverterRegistry, supplemented with our custom converter
ValueConverterRegistry valueConverterRegistry =
ValueConverterRegistry.withDefaultsSupplementedBy(Set.of(jwtVc));
// Configure Soklet to use our registry
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).valueConverterRegistry(valueConverterRegistry)
.build();
// Create a ValueConverterRegistry, supplemented with our custom converter
ValueConverterRegistry valueConverterRegistry =
ValueConverterRegistry.withDefaultsSupplementedBy(Set.of(jwtVc));
// Configure Soklet to use our registry
SokletConfig config = SokletConfig.withServer(
Server.withPort(8080).build()
).valueConverterRegistry(valueConverterRegistry)
.build();
Everything is in place. We can now accept our new Jwt type anywhere that Soklet injects values.
@GET("/jwt/{jwt}/payload")
public String jwtPayload(@PathParameter Jwt jwt) {
return jwt.payload();
}
@GET("/jwt/{jwt}/payload")
public String jwtPayload(@PathParameter Jwt jwt) {
return jwt.payload();
}
Verify that it works:
% curl -i 'http://localhost:8080/jwt/a.b.c/payload'
HTTP/1.1 200 OK
Content-Length: 1
Content-Type: text/plain;charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
b
% curl -i 'http://localhost:8080/jwt/a.b.c/payload'
HTTP/1.1 200 OK
Content-Length: 1
Content-Type: text/plain;charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
b
And verify behavior if conversion fails:
% curl -i 'http://localhost:8080/jwt/abc/payload'
HTTP/1.1 400 Bad Request
Content-Length: 21
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
HTTP 400: Bad Request
% curl -i 'http://localhost:8080/jwt/abc/payload'
HTTP/1.1 400 Bad Request
Content-Length: 21
Content-Type: text/plain; charset=UTF-8
Date: Sun, 21 Mar 2024 16:19:01 GMT
HTTP 400: Bad Request
We can see that if an exception occurs during value conversion, Soklet detects this and re-throws a subclass of BadRequestException with a detailed description of the problem and fields that contain the offending name and value (in this case, IllegalPathParameterException). Your exception handling code will have everything it needs to gracefully process the error and surface a user-friendly error message.
com.soklet.exception.IllegalPathParameterException: Illegal value 'abc' was specified for path parameter 'jwt' (was expecting a value convertible to class com.soklet.example.Jwt)
at com.soklet.DefaultResourceMethodParameterProvider.extractParameterValueToPassToResourceMethod(DefaultResourceMethodParameterProvider.java:164)
...
Caused by: com.soklet.converter.ValueConversionException: Unable to convert value 'abc' of type class java.lang.String to an instance of class com.soklet.example.Jwt
...
Caused by: java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
at com.soklet.example.AppModule$1.performConversion(AppModule.java:120)
at com.soklet.example.AppModule$1.performConversion(AppModule.java:116)
at com.soklet.converter.AbstractValueConverter.convert(AbstractValueConverter.java:96)
com.soklet.exception.IllegalPathParameterException: Illegal value 'abc' was specified for path parameter 'jwt' (was expecting a value convertible to class com.soklet.example.Jwt)
at com.soklet.DefaultResourceMethodParameterProvider.extractParameterValueToPassToResourceMethod(DefaultResourceMethodParameterProvider.java:164)
...
Caused by: com.soklet.converter.ValueConversionException: Unable to convert value 'abc' of type class java.lang.String to an instance of class com.soklet.example.Jwt
...
Caused by: java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
at com.soklet.example.AppModule$1.performConversion(AppModule.java:120)
at com.soklet.example.AppModule$1.performConversion(AppModule.java:116)
at com.soklet.converter.AbstractValueConverter.convert(AbstractValueConverter.java:96)

