private Result<AbstractConfig> validateClassLevelConstraints(final PropertyPath propertyPath,
                                                                 final String effectiveValueStr,
                                                                 final AbstractConfig parentConfigObject) {
        // This is the config object before we have applied the update, i.e. the before picture
        final AbstractConfig config = configMapper.getConfigObject(parentConfigObject.getClass());

        // Get info about the config class, i.e. ctor, prop names, etc.
        final ObjectInfo<AbstractConfig> objectInfo = PropertyUtil.getObjectInfo(
                new ObjectMapper(),
                propertyPath.getParentPropertyName()
                        .orElse(null),
                config);

        // Build a map of all the prop de-serialised values. Can't use Collectors.toMap due to null values
        final Map<String, Object> nameToValueMap = new HashMap<>();
        objectInfo.getPropertyMap().forEach((propName, prop) ->
                nameToValueMap.put(propName, prop.getValueFromConfigObject()));

        final Object newValue = configMapper.convertValue(propertyPath, effectiveValueStr);
        // Set the prop being validated to its new value
        nameToValueMap.put(propertyPath.getPropertyName(), newValue);

        // Create a new config obj using the new value so we can validate the whole thing, which
        // allows us to get cross-property violations
        final AbstractConfig configCopy = objectInfo.createInstance(nameToValueMap::get);

        return appConfigValidator.validate(configCopy);
    }
@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