public static <T> void logConstraintViolation(
            final ConstraintViolation<? extends T> constraintViolation,
            final ValidationSeverity severity,
            final Class<T> configSuperType) {

        final Consumer<String> logFunc;
        final String severityStr;
        if (severity.equals(ValidationSeverity.WARNING)) {
            logFunc = LOGGER::warn;
            severityStr = "warning";
        } else {
            logFunc = LOGGER::error;
            severityStr = "error";
        }

        String propName = null;
        for (jakarta.validation.Path.Node node : constraintViolation.getPropertyPath()) {
            propName = node.getName();
        }
        final T config = (T) constraintViolation.getLeafBean();

        final String path;
        if (config instanceof HasPropertyPath) {
            final HasPropertyPath hasPropertyPath = ((HasPropertyPath) config);
            final String propPath = hasPropertyPath.getFullPathStr(propName);
            path = propPath != null
                    ? propPath
                    : config.getClass().getName();
        } else {
            // No path so make do with the class name
            path = config.getClass().getName();
        }
        // Value might be a collection so strip the square brackets as
        final String valueStr = NullSafe.getOrElse(
                constraintViolation.getInvalidValue(),
                Object::toString,
                str -> str.startsWith("[") && str.endsWith("]")
                        ? str
                        : "[" + str + "]",
                "[null]");

        logFunc.accept(LogUtil.message("  Validation {} for property {} with value {} - {}",
                severityStr,
                path,
                valueStr,
                constraintViolation.getMessage()));
    }
Walks the config object tree validating each branch regardless of whether they have @Valid
annotations.
stroom-util/src/main/java/stroom/util/config/ConfigValidator.java