RecaptchaSiteVerifyResponse.java
/*
* Copyright (c) 2007-2026 MetaSolutions AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.entrystore.rest.springboot.model.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Objects;
/**
* Response of the reCaptcha {@code siteverify} endpoint.
*
* <p>{@code ignoreUnknown} because the shape is Google's to change: the response also carries
* {@code challenge_ts}, {@code hostname} and, for reCaptcha Enterprise, further members none of which
* this application reads. Failing on them would turn an upstream addition into a sign-up outage, and
* relying on the application mapper's lenient default would make that depend on configuration
* elsewhere.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record RecaptchaSiteVerifyResponse(
/**
* Boxed, not {@code boolean}: Jackson 3 enables {@code FAIL_ON_NULL_FOR_PRIMITIVES}, and a
* creator parameter is handed null both when the member is absent and when it is an explicit
* {@code null}, so a primitive here throws while the body is being read rather than resolving to
* "not verified" — turning a malformed upstream reply into a 500 instead of a rejected captcha.
* Verified against this build with a {@code {}} body.
*
* <p>The constructor folds both cases to false, so the accessor never returns null and callers
* can unbox it safely.
*/
Boolean success,
/**
* Machine-readable reasons for a rejection, e.g. {@code invalid-input-secret} for a
* misconfigured private key versus {@code timeout-or-duplicate} for a stale token. Absent from a
* successful response.
*/
@JsonProperty("error-codes") List<String> errorCodes) {
public RecaptchaSiteVerifyResponse {
// Absent or explicitly null means not verified, and folding it here is what lets the accessor
// return a value that never unboxes to a NullPointerException at the call site.
success = success != null && success;
// Nulls filtered rather than passed to List.copyOf, which rejects them: a null element would
// throw inside the deserializer, and that failure surfaces from RestClient as a plain
// RestClientException — the same shape as an unreadable body, so it would answer 500 rather than
// a rejected captcha.
errorCodes = errorCodes == null ? List.of()
: errorCodes.stream().filter(Objects::nonNull).toList();
}
}