JSON and profiles
View MarkdownVertique lets you define a JSON mapping policy once, give it a name, and select it where data enters or leaves your application.
- Different contracts in one application. Use one profile for a public API and another for a partner integration, with independent naming, date, number, and deserialization settings.
- Reusable profiles across integrations. REST APIs, REST clients, and Kafka JSON bindings resolve named profiles through the same registry. Select a profile with an annotation or configuration instead of constructing a mapper at every call site.
- Validated configuration. Duplicate profile names, reserved names, unknown configured defaults, and custom mappers that cannot round-trip Vert.x JSON values fail startup.
Use profiles when integrations need different JSON representations or when several integrations must share the same mapping rules. If the built-in behavior fits your contracts, select a built-in profile and keep your application code focused on its domain types. Profiles configure Jackson mapping. They do not replace request validation or your API schema.
MCP tool arguments and structured results also use the profile registry. MCP protocol envelopes stay independent of the tool’s profile.
Select a profile for a REST method
Section titled “Select a profile for a REST method”The hello example uses vertique-strict on its price endpoint so typed decimal values are
serialized as JSON strings. This excerpt from JsonProfilesDemoResource omits OpenAPI annotations
and the alternate response branch:
@GET@Path("/price")@Produces(MediaType.APPLICATION_JSON)@JsonProfile("vertique-strict")public Future<PriceQuote> getPriceQuote( @QueryParam("nodiscount") @DefaultValue("false") boolean nodiscount) { // ... alternate response when nodiscount is true return Future.succeededFuture( new PriceQuote("SKU-1", new BigDecimal("1.50"), Optional.of(new BigDecimal("0.25"))));}The response contains "amount":"1.50" and "discount":"0.25". The same example’s
JsonProfilesDemoIT asserts those strings and checks that an empty discount is omitted.
The Java properties remain BigDecimal; the profile determines their JSON representation.
For a JAX-RS resource, method-level @JsonProfile overrides the class annotation. It applies to
both request-body mapping and response serialization for that method. See REST APIs
for resource registration and request validation.
Built-in profiles
Section titled “Built-in profiles”| Profile | Behavior |
|---|---|
vertique |
ISO-8601 dates and Java time support; Optional values unwrap, null-valued properties and empty Optional properties are omitted. Typed decimals remain JSON numbers. |
vertique-strict |
Adds decimal strings for typed BigDecimal values and rejects number/boolean-to-String coercion. Use it when the JSON contract requires these restrictions. |
The third built-in profile, system, supplies baseline Java time, Optional, and Vert.x JSON type
support without Vertique’s null-inclusion, number, or enum preferences. The integration default is
vertique, and the process JSON codec defaults to system. The profile name vertx is retired.
Configuring it fails startup with a message that names system as the replacement.
vertique-strict accepts bounded plain decimal strings and does not accept exponent notation.
Under this profile, untyped decimal JSON numbers remain numbers and bind as doubles. Declare a
BigDecimal property when arbitrary precision matters. The JSON module reference
documents decimal bounds, Optional behavior, enum handling, and the complete profile recipes.
Choose profiles at each integration
Section titled “Choose profiles at each integration”You can reuse the same named profile across these integrations:
| Integration | Local selection | Integration-wide default |
|---|---|---|
| REST API | @JsonProfile on the resource method or class |
jaxrs.jsonProfile |
| REST client | Client interface annotation, builder selection, or named-client configuration | restClient.defaults.jsonProfile |
| Kafka consumer | Listener annotation or consumer-binding configuration | kafka.jsonProfile |
| Kafka producer | Producer annotation, producer configuration, or per-method configuration | kafka.jsonProfile |
MCP uses @JsonProfile on a tool method or declaring type, with mcp.jsonProfile as its
integration-wide default. See MCP for tool wiring.
The global default is json.jsonProfile. A more specific selection takes precedence, and the
exact order depends on the integration. For example, named REST-client configuration overrides its
interface annotation, and an explicitly supplied client mapper takes precedence over profiles.
The module reference lists the full resolution order.
This configuration, from the JSON module reference, selects vertique as the global integration
default while allowing local overrides:
{ "json": { "jsonProfile": "vertique" }}Put this in the application configuration loaded through Configuration. An unknown configured default fails startup even when individual bindings override it.
Define an application profile
Section titled “Define an application profile”A profile pairs a JsonProfileId with a configured ObjectMapper. Register it through a Dagger
@IntoSet JsonMapperProfile binding, then select its name through the same annotations and
configuration as a built-in profile. Include the contributing Dagger module in your application
component. Configure the mapper before registration and treat it as immutable afterward.
The MCP example defines a profile that uses snake case. Its ExampleJsonProfile.create() method is:
public static JsonMapperProfile create() { var mapper = JacksonDefaults.applySystem(new ObjectMapper()); mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); return JsonMapperProfiles.of(JsonProfileId.of(ID), mapper);}ID is "weather-example". The example contributes it from McpExampleModule:
@Provides@IntoSetstatic JsonMapperProfile exampleJsonProfile() { return ExampleJsonProfile.create();}Its weather tool selects @JsonProfile(ExampleJsonProfile.ID). The profile can also be selected
by a REST or Kafka integration in the same application. Starting from
JacksonDefaults.applySystem(new ObjectMapper()) preserves the baseline type support and read
limits while allowing the application to choose its own mapping policy. The
JSON module reference
shows the complete Dagger binding. An application profile must preserve JsonObject and
JsonArray structure in the registry’s startup round-trip probe.
The REST, REST-client, and Kafka JSON modules install JsonRuntimeModule through their own
Dagger wiring. If you assemble a JSON-only application graph, install that runtime module
explicitly. Profile contracts live in vertique-core; their registry and mapper implementations
come from vertique-json.
Integration profiles and the process codec
Section titled “Integration profiles and the process codec”json.jsonProfile and json.systemProfile have different scopes:
json.jsonProfilesets the fallback for integrations that select a profile.json.systemProfileselects the process-wide mapper behind Vert.xJsonandJsonObjectoperations.JsonRuntimeModuleinstalls it during the CONFIGURE startup phase.
Changing a resource’s profile leaves other integrations on their own selections. Changing the
process codec can affect shared JSON operations, cache entries, and persisted state throughout
the JVM. Keep the default system codec unless you intend that broader format change.
system adds Java time and Optional support. Legacy Date and Calendar values write as
ISO-8601 strings, where raw Vert.x writes epoch milliseconds.
The request schema follows the profile
Section titled “The request schema follows the profile”The default web-validation strategy builds each route’s request-body schema through that
route’s own effective profile, so the schema describes the wire shape the profile’s mapper
parses. Selecting the profile is all the configuration this needs.
- Strict decimals are strings. On a
vertique-strictroute a decimal property is typed as a string with that profile’s decimal grammar and length bound. The strict string form is accepted. A JSON number, an exponent form, and an over-length value are rejected with a 400 before your resource runs. A route on a profile that declares no such override keeps the JSON-number shape. - Do not annotate a strict decimal with
@Schema(implementation = String.class). On a route whose profile declares an override for the property’s type, that annotation fails router construction with a message naming the operation and the property. The profile already supplies the string form together with its grammar and length bound. - A repeated key is rejected under
vertique-strict. A route on that profile answers 400 to{"quantity":1,"quantity":2}before the resource runs.systemandvertiquekeep the last value. - The schema describes what the binder reads. It covers every property Jackson’s resolved
deserializer binds: a Lombok builder method, a creator-parameter constructor, an any-setter’s
extra keys, an aliased spelling. A builder-only request body, a getter-only collection, and a
renamed creator parameter are therefore described and validated. On input, the accepted wire
name is always the name Jackson binds. A
@Schema(name = ...)rename does not change it. - Aliases and typed extra keys are checked. The gate validates the alias spellings of a
described property (those that name no other property of the same type) and the typed extra
keys of a body that accepts them. A value the parser alone would have coerced is a 400. Under
vertique-strict, a body that carries one property under two spellings is also rejected. - Map values are described. A value inside an ordinary map property is described by the value
type’s own schema, including a size or pattern constraint declared on the value type. A
wrong-typed or constraint-violating value inside the map is a 400 before your resource runs. A
value type of
Objector a raw JSON tree stays open. Anullinside a map whose values are not Optional is rejected as wrong-typed, and a map or extras position whose values are Optional accepts an explicitnull. A map value type that would be refused at startup as a named property, such as one with a custom deserializer or a delegating creator, is refused inside a map as well. The JSON Schema module reference describes the profile-override remedy. - Extra keys can be closed per member. A member-level
@Schema(additionalProperties = FALSE)on a property whose value type accepts extra keys closes that member, while the same value type stays open elsewhere. When a parent and an unwrapped child both accept extra keys under one shared key, both value constraints are enforced together. - Some constraints need
vertique-validation.@Positive,@PositiveOrZero,@Negative, and@NegativeOrZeroare enforced without a validator on a field, a getter, a constructor or static-factory parameter, a builder method, and a setter through its backing field.@Emailand Hibernate-specific constraints on a constructor parameter, builder method, or setter need the validator, with the constraint declared on the backing field. With a Bean ValidationValidatoron the application’s Dagger graph, the generated schema also gains constructor-parameter, XML-mapped, and interface-inherited constraints, and it corrects four Hibernate Validator shapes:@Range,@Length,@URL, and a@Patternflag. The validator only fills a keyword that was unset or corrects one of those four shapes. It does not loosen a constraint the schema already enforced.
The JSON Schema module reference lists the shapes the schema does not describe.
This applies to body schemas. Query, path, header, and cookie parameters are assembled from their declared types and constraints, and response serialization is not affected.
Use the openapi-contract strategy instead when the generated openapi.json must itself be the
validating authority, so that the published contract and the runtime check cannot diverge. The
hello example does that, with BigDecimalModelConverter in its spec build. The build-time spec
generator is a separate generator with its own annotation support. The runtime synthesizer
described here is the one web-validation enforces.
Follow the JSON module reference for the supported options and their scope, and the REST validation module reference for the described property shapes in full. Test accepted and rejected request bodies as well as the JSON your application writes.
Learn more
Section titled “Learn more”- JSON module reference: profile selection, configuration, mapper recipes, validation, and keyed collections.
- JSON Schema module reference: the deserializer-driven property model, the Bean Validation metadata supplement, and their documented gaps.
- JSON profile example resource: the full REST example behind the price excerpt.
- JSON profile integration tests: response shape and request acceptance assertions.
- Custom profile example and Dagger contribution: the reusable snake-case profile.
- REST clients and Kafka: outbound and messaging integration setup.
Continue reading
Section titled “Continue reading”- Previous: Configuration
- Next: AOP and events