Skip to content

JSON and profiles

View Markdown

Vertique 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.

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.

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.

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.

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
@IntoSet
static 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.jsonProfile sets the fallback for integrations that select a profile.
  • json.systemProfile selects the process-wide mapper behind Vert.x Json and JsonObject operations. JsonRuntimeModule installs 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 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-strict route 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. system and vertique keep 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 Object or a raw JSON tree stays open. A null inside a map whose values are not Optional is rejected as wrong-typed, and a map or extras position whose values are Optional accepts an explicit null. 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 @NegativeOrZero are 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. @Email and 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 Validation Validator on 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 @Pattern flag. 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.

  • 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.