Validation & JSON Binding
In Phase 4 you built a JAX-RS resource that takes an incoming Product from a
request body and hands it to your service. There's a quiet assumption buried in that code: that the
Product arriving over the wire is sane. A real client will eventually send you a product with a blank
name, a negative price, or a sku that's a single character — by accident or on purpose. Where do you
catch that?
The mental model to hold onto for this whole phase: two standards bracket your method, one on each side
of the wire. On the way in, raw JSON bytes arrive and something has to turn them into a Product
object — that's JSON-B, the binding spec. Then, before your code runs, something checks that the
object actually makes sense — that's Bean Validation, the constraint spec. On the way out, JSON-B
runs again, turning your Product back into JSON. You don't call either one by hand inside a JAX-RS
resource; you declare what you want with annotations and let the container do the work. Same inversion of
control you've seen all guide: you label, the framework acts.
📝 If you've used Spring's @Valid and @NotBlank
(The Service Layer, DTOs & Validation),
you already know this — because it's the same spec. Bean Validation is a Jakarta standard; Spring
implements it too. The annotations (@NotBlank, @Size, @Positive) are identical down to the package
name. What changes here is the wiring: instead of a Spring @RestController, the rules plug into a
JAX-RS resource, and instead of Jackson, the JSON mapper is JSON-B.
Jakarta Bean Validation — rules that live on the data
📝 Jakarta Bean Validation is the standard way to declare constraints with annotations directly on your
fields. Instead of a pile of if statements scattered across your methods, you write the rule once,
right next to the field it describes, and the constraint travels with the data wherever it goes. The
common constraints:
@NotNull— the value must not be null.@NotBlank— a string must be non-null and contain at least one non-whitespace character.@Size(min=, max=)— a string (or collection) length must fall in range.@Min/@Max— a number must be at least / at most a value.@Positive/@PositiveOrZero— a number must be greater than (or equal to) zero.@Email— a string must look like an email address.
Here's the Product from Phase 4, now wearing its rules:
What just happened: Each constraint is a declaration of intent sitting on the field it guards. name
must be present and non-blank and not absurdly long; price must exist and be positive (you don't sell
things for negative money); sku must be a reasonable length. Notice the id has no constraints — it's
assigned by the server on create, not sent by the client, so there's nothing to validate. None of these
annotations does anything on its own yet; they're metadata describing what a valid Product looks like.
The next section is the switch that makes the container actually enforce them.
Validating in JAX-RS — @Valid at the boundary
📝 @Valid on a resource method's body parameter tells the container to validate the incoming object
before your method body runs. The flow: JSON-B deserializes the request body into a Product, then —
because of @Valid — Bean Validation checks every constraint on it. If anything fails, your code never
executes; the container short-circuits and returns a 400 Bad Request describing the violations. If
everything passes, your method runs with an object you can trust.
What just happened: The only new token versus Phase 4 is @Valid in front of the Product parameter.
That single annotation is the switch — it tells JAX-RS "after JSON-B builds this object, run its
constraints, and if any fail, don't call me." The body of createProduct is now allowed to assume a clean
product, because it can't be reached any other way. Without @Valid, the annotations on Product are
inert metadata and a blank-name product would sail straight into service.create(...).
Send a bad request — a blank name and a negative price:
The container rejects it before your code runs, with a 400 and the violations:
What just happened: You wrote zero if statements, yet the request was rejected with a clear 400 listing
exactly which fields failed and the message strings you authored on the constraints. The exact JSON shape
of this error body varies by application server (WildFly, Payara, and Open Liberty differ in the wrapper
and the field naming), but the principle is universal: violations come back as a 400 before your handler
is entered. In practice you'd map these to a tidy, consistent shape with an ExceptionMapper — the JAX-RS
hook for turning exceptions into responses — but the default already protects you.
💡 This is the Jakarta mirror of Spring's @Valid @RequestBody — same annotation, same 400-on-failure
behavior, different doorway. The skill transfers directly.
Validating elsewhere — not just at the HTTP edge
The HTTP boundary is the most common place to validate, but it isn't the only one. @Valid and the
constraint annotations work anywhere the container manages the call.
📝 You can put @Valid (and even bare constraints) on CDI or EJB method parameters, and the container
will validate them on every call — this is method-level validation. A ProductService.create can demand
a valid argument no matter who calls it, not just HTTP traffic:
What just happened: The @Valid here means a scheduled job, a message listener, or another service that
calls create gets the same guarantee the HTTP layer does — the container validates the argument before
the method body runs. The rule no longer depends on someone remembering to validate at the edge.
You can also validate programmatically when you need full control — inject a Validator and ask it
directly:
private Validator validator;
public void
What just happened: validator.validate(product) runs the constraints and hands back a Set of whatever
failed (empty means valid). This is the manual escape hatch for when annotations on a parameter aren't
enough — say you need to validate conditionally, or collect violations to report in a custom way. Most of
the time @Valid is all you need; reach for the Validator only when you genuinely need to drive
validation yourself.
JSON-B — mapping objects to and from JSON
You've leaned on JSON-B since Phase 4 without configuring it. Time to take the wheel.
📝 JSON-B (Jakarta JSON Binding) is the standard object↔JSON mapper — the spec's built-in translator, the role Jackson plays in the Spring world. JAX-RS uses it automatically: it serializes your return values to JSON and deserializes incoming JSON into Java objects. Out of the box it maps field names straight across. When the JSON shape needs to differ from your Java shape, a few annotations adjust it:
@JsonbProperty("name")— use a different JSON key for this field.@JsonbTransient— exclude this field from JSON entirely.@JsonbDateFormat/@JsonbNumberFormat— control how dates and numbers are rendered.
What just happened: @JsonbProperty("stockKeepingUnit") decouples the wire name from the Java field —
the JSON key becomes stockKeepingUnit while your code still calls it sku. @JsonbTransient on
internalCostCode keeps that field out of the JSON completely, so an internal value can never leak to a
client. Everything else maps by field name as before.
That Product serializes to:
What just happened: sku came out as stockKeepingUnit exactly as the annotation directed, and
internalCostCode is nowhere in the output — @JsonbTransient did its job. The other fields mapped
straight across. You changed the public contract without touching a single line of serialization code.
📝 One layer below JSON-B sits JSON-P (Jakarta JSON Processing) — a low-level API for reading and
writing JSON as a stream or tree (JsonObject, JsonParser) without binding to Java classes. You reach
for it when you're handling JSON whose shape you don't know ahead of time, or when you want streaming
control. For mapping known objects — which is almost always — JSON-B is the right tool, and it's built on
JSON-P under the hood.
Custom constraints, and how the pieces snap together
The built-in constraints cover most needs, but sometimes a rule is specific to your domain. Say a valid
sku must match a particular pattern your catalog uses. You can build your own constraint.
📝 A custom constraint is an annotation paired with a ConstraintValidator. You define the annotation,
point it at a validator class, and from then on it behaves exactly like @NotBlank — usable on any field,
enforced by @Valid.
What just happened: The @ValidSku annotation is wired to ValidSkuValidator via
@Constraint(validatedBy = ...). The validator's isValid returns true for a good value and false to
trigger a violation — here it checks the SKU pattern. Returning true for null is the convention: each
constraint should do one job, so "is it present?" stays the responsibility of @NotNull. Drop @ValidSku
on the sku field and it now participates in @Valid like any built-in rule.
💡 Step back and watch the full request flow, because every standard in this guide has a place in it:
flowchart TD
A[HTTP request with JSON body] --> B[JSON-B deserializes JSON to Product]
B --> C["Bean Validation checks @Valid constraints"]
C -->|invalid| D[400 Bad Request - your code never runs]
C -->|valid| E[CDI ProductService runs in @Transactional]
E --> F[JPA persists the row]
F --> G[JSON-B serializes the result back to JSON]
JSON-B turns bytes into an object, Bean Validation guards the gate, your CDI service (from
Phase 3) does the work inside a @Transactional (from
Phase 6), JPA (from Phase 5) writes the row,
and JSON-B serializes the answer. Each spec owns one slice; together they're the spine of a Jakarta API.
⚠️ Validate at the boundary — don't trust client input. It is tempting to assume "my front-end already
checks the form, so the server can relax." It can't. Anyone can send a raw HTTP request that skips your
front-end entirely: a curl command, a buggy mobile client, a malicious script. The server is the only
place you actually control, so the constraints on your Product plus a @Valid at the edge are not
belt-and-suspenders — they're the belt. Bad data that slips past validation becomes corrupt rows, and
corrupt rows outlive the bug that created them.
Recap
- Bean Validation puts rules on the data. Annotate fields with
@NotBlank,@NotNull,@Size,@Positive,@Min/@Max,@Email— the rule lives next to the field and travels with the object. It's the same spec Spring uses, down to the package names. @Validenforces them in JAX-RS. Put@Validon the body parameter; the container validates the deserialized object before your method runs and returns a 400 with the violations if it fails — your code never sees bad input.- Validation isn't only for HTTP.
@Validworks on CDI/EJB method parameters too (method-level validation), and you can inject aValidatorto validate programmatically when you need full control. - JSON-B is the standard object↔JSON mapper. JAX-RS uses it automatically;
@JsonbPropertyrenames a key,@JsonbTransienthides a field, and date/number format annotations shape the output. JSON-P sits below it for low-level streaming when you don't have a class to bind to. - Custom constraints are annotation +
ConstraintValidator. Define your own rule (like a SKU format check) and it plugs into@Validexactly like a built-in. ⚠️ Always validate at the boundary — the client can't be trusted, and the server is the only gate you control.
Quick check
Make sure the validation and binding ideas stuck:
[
{
"q": "You annotated Product.name with @NotBlank, but blank names still reach your service and crash it. What did you most likely forget?",
"choices": [
"Adding @Valid to the Product parameter on the resource method, which is the switch that tells the container to actually enforce the constraints",
"Importing the jakarta.validation package into the Product class",
"Annotating the service with @Transactional so validation can roll back",
"Setting @Produces(APPLICATION_JSON) on the method"
],
"answer": 0,
"explain": "Constraint annotations on a field are just metadata until something triggers them. In JAX-RS, @Valid on the body parameter is that trigger — it tells the container to validate the deserialized object before your method runs. Without @Valid, the rules are inert and bad input passes straight through."
},
{
"q": "What is the role of JSON-B (Jakarta JSON Binding) in a JAX-RS request?",
"choices": [
"It serializes your return values to JSON and deserializes incoming JSON into Java objects — the standard object-to-JSON mapper, the role Jackson plays in Spring",
"It validates incoming objects against their constraint annotations",
"It manages the database transaction around the request",
"It routes the request to the correct resource method based on the URL"
],
"answer": 0,
"explain": "JSON-B is the binding spec: object↔JSON. JAX-RS uses it automatically to turn the request body into a Java object and your return value back into JSON. Validation is Bean Validation's job (a separate spec), transactions are JTA's, and routing is JAX-RS's @Path matching."
},
{
"q": "You need a reusable custom rule that checks a SKU matches your catalog's pattern, usable on any field via @Valid. What do you build?",
"choices": [
"A custom annotation paired with a class implementing ConstraintValidator, wired together with @Constraint(validatedBy = ...)",
"A subclass of Product that overrides a validate() method",
"An ExceptionMapper that inspects the SKU and throws on a bad value",
"A @JsonbProperty annotation with a regex argument"
],
"answer": 0,
"explain": "A custom constraint is an annotation (marked @Constraint and pointed at a validator) plus a ConstraintValidator implementation whose isValid does the check. Once defined, it plugs into @Valid like any built-in constraint. JSON-B annotations control JSON shape, not validation, and ExceptionMapper handles errors, not the rule itself."
}
]
← Phase 6: Transactions with JTA · Guide overview · Phase 8: Enterprise Beans & Messaging →
Check your understanding
1. You annotated Product.name with @NotBlank, but blank names still reach your service and crash it. What did you most likely forget?
2. What is the role of JSON-B (Jakarta JSON Binding) in a JAX-RS request?
3. You need a reusable custom rule that checks a SKU matches your catalog's pattern, usable on any field via @Valid. What do you build?