private void validateConfigProperty(final ConfigProperty configProperty) {
        // We need to validate the effective value as the DB value may have been set to null
        // and the validation may demand NotNull. In this instance the yaml/default should provide
        // a value to satisfy the validation.

        final PropertyPath propertyPath = configProperty.getName();
        final String effectiveValueStr = configProperty.getEffectiveValue().orElse(null);
        try {
            configMapper.validateValueSerialisation(propertyPath, effectiveValueStr);
        } catch (Exception e) {
            throw new ConfigPropertyValidationException(LogUtil.message("Error parsing [{}]: {}",
                    effectiveValueStr, e.getMessage(), e));
        }

        final PropertyUtil.Prop prop = configMapper.getProp(propertyPath)
                .orElseThrow(() ->
                        new RuntimeException(LogUtil.message("No prop object exists for {}",
                                configProperty.getName())));

        final AbstractConfig parentConfigObject = (AbstractConfig) prop.getParentObject();
        Objects.requireNonNull(parentConfigObject);

        final Result<AbstractConfig> validationResult;
        try {
            if (parentConfigObject.getClass().isAnnotationPresent(NotInjectableConfig.class)) {
                // Classes that are not injectable, like the many CacheConfig ones, we cannot inject an instance
                // for them so we can't do whole class validation. In theory, we could walk back up the tree
                // of objects to perform class level validation on an ancestor that is injectable but it all
                // get very messy and fiddly.
                final Object newValue = configMapper.convertValue(propertyPath, effectiveValueStr);
                validationResult = appConfigValidator.validateValue(
                        parentConfigObject.getClass(), propertyPath.getPropertyName(), newValue);
            } else {
                validationResult = validateClassLevelConstraints(
                        propertyPath,
                        effectiveValueStr,
                        parentConfigObject);
            }
        } catch (Exception e) {
            LOGGER.error("Error validating prop {} with value '{}': {}",
                    propertyPath, effectiveValueStr, e.getMessage(), e);
            throw new RuntimeException(e);
        }

        // TODO ideally we would handle warnings in some way, but that is probably a job for a new UI
        if (validationResult.hasErrors()) {
            // We may have more than one message for the one prop, as each prop can have many validation annotations
            final StringBuilder stringBuilder = new StringBuilder()
                    .append("Value [").append(effectiveValueStr).append("] ")
                    .append(" for property ")
                    .append(propertyPath.toString())
                    .append(" is invalid:");

            validationResult.handleErrors(error -> {
                stringBuilder
                        .append("\n")
                        .append(error.getMessage());
            });
            throw new ConfigPropertyValidationException(stringBuilder.toString());
        }
    }
@param id The DB primary key for the prop
@return A {@link ConfigProperty} if it exists in the DB, i.e. it has a db override value.
This means a valid prop can return an empty if the prop only has a default/yaml value.
stroom-config/stroom-config-global-impl/src/main/java/stroom/config/global/impl/GlobalConfigService.java