3

I have some value from my configuration file, that should be a JSON (which will be loaded as a String).

I'd like Spring to validate that this value is indeed a valid JSON before injecting it and throw an error else.

I'm injecting it as follows:

@Value("${source.SomeJsonString}")
private String someJsonString;

I saw the following: How to make simple property validation when using Spring @Value

However, since I have multiple classes that should be injected with source.SomeJsonString, I wouldn't like to create a setter for each, and write the validation again and again.

Is there any way to write the validator only once?

I thought about creating annotation (Spring validate string value is a JSON), but it seems that values that are annotated with @Value cannot be validated .

Is there any other way?

ChikChak
  • 936
  • 19
  • 44
  • 3
    Inject it once and only once, in one bean, and inject that bean wherever you need access to the value. If using Spring Boot, you shouldn't even use Value. You should use a bean annotated with ConfigurationProperties instead. – JB Nizet Dec 07 '19 at 13:48
  • *but it seems that values that are annotated with @Value cannot be validated* - not true. Validate it 1) in constructor - using constructor injection 2) In \@PostConstruct 3) in setter logic - using setter injection. – Antoniossss Dec 07 '19 at 15:21
  • Besides it still allows JSR validation (@NotNull, @Pattern etc). – Antoniossss Dec 07 '19 at 15:21
  • @Antoniossss Both of your solutions will force me to rewrite the same logic for every class that uses this value. – ChikChak Dec 07 '19 at 22:20
  • @JBNizet So I should create a class annotated with ConfigurationProperties that will hold the value of the JSON string? and inject that class to everyone who want to access to the JSON string? – ChikChak Dec 07 '19 at 22:24
  • @ChikChak a littlebit - but its not my fault you are using invalid value across application. – Antoniossss Dec 07 '19 at 22:25

2 Answers2

5

Spring externalized configuration can be validated using JSR 303 Bean Validation API. But it requires Spring Type-safe Configuration Properties instead of @Value("${property}").

Add Hibernate Validator dependency to build.gradle

implementation 'org.hibernate.validator:hibernate-validator'

The type-safe configuration properties must be annotated with @Validated and the field someJsonString with a custom annotation @ValidJsonConstraint

@Component
@ConfigurationProperties("source")
@Validated
public class SourceProperties {

  @ValidJsonConstraint
  private String someJsonString;

  public String getSomeJsonString() {
    return someJsonString;
  }

  public void setSomeJsonString(String someJsonString) {
    this.someJsonString = someJsonString;
  }
}

You can inject the properties into all required services, so the validation code is not duplicated

@Autowired
private SourceProperties sourceProperties;

It's time to create the custom annotation

@Documented
@Constraint(validatedBy = ValidJsonValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidJsonConstraint {

  String message() default "Invalid JSON";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};
}

and a validator to validate fields annotated with the custom annotation

public class ValidJsonValidator implements ConstraintValidator<ValidJsonConstraint, String> {

  private final ObjectMapper objectMapper = new ObjectMapper();

  @Override
  public void initialize(ValidJsonConstraint constraintAnnotation) {
  }

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    try {
      objectMapper.readTree(value);
      return true;
    } catch (JsonProcessingException e) {
      return false;
    }
  }
}

When in the application.properties the source.someJsonString value is valid JSON

source.someJsonString={"test":"qwe"}

application successfully starts.

When JSON is invalid

source.someJsonString=qwe

Application fails to start with the following exception

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'source' to intellectsoft.afgruppen.shiftschedule.SourceProperties failed:

    Property: source.someJsonString
    Value: qwe
    Origin: class path resource [application.properties]:26:23
    Reason: Invalid JSON


Action:

Update your application's configuration

Also, the same can be achieved a bit easier without JSR 303 Bean Validation API.

Create a custom validation component

@Component
public class JsonValidator {

  private final ObjectMapper objectMapper = new ObjectMapper();

  public boolean isValid(String value) {
    try {
      objectMapper.readTree(value);
      return true;
    } catch (JsonProcessingException e) {
      return false;
    }
  }
}

Inject the validator and perform validation in the property setter

@Component
@ConfigurationProperties("source")
public class SourceProperties {

  private final JsonValidator jsonValidator;

  private String someJsonString;

  public SourceProperties(JsonValidator jsonValidator) {
    this.jsonValidator = jsonValidator;
  }

  public String getSomeJsonString() {
    return someJsonString;
  }

  public void setSomeJsonString(String someJsonString) {
    if (!jsonValidator.isValid(someJsonString)) {
      throw new IllegalArgumentException(someJsonString + " is not a valid JSON");
    }
    this.someJsonString = someJsonString;
  }
}
Eugene Khyst
  • 9,236
  • 7
  • 38
  • 65
-1

You could insert the string as a bean, parsing/validating it first:

@Configuration
public class JsonStringPropertyConfig {

    @Autowired
    private ObjectMapper objectMapper;

    @Bean
    @Qualifier("someJsonString")
    String someJsonString(@Value("${source.someJsonString}") String someJsonString) throws JsonProcessingException {
        // Validate
        objectMapper.readTree(someJsonString);
        return someJsonString;
    }

}
@Service
public class SomeService {

    @Autowired
    @Qualifier("someJsonString")
    private String someJsonString;

}

or even simpler, with the @Resource annotation:

@Configuration
public class JsonStringPropertyConfig {

    @Autowired
    private ObjectMapper objectMapper;

    @Bean
    String someJsonString(@Value("${source.someJsonString}") String someJsonString) throws JsonProcessingException {
        // Validate
        objectMapper.readTree(someJsonString);
        return someJsonString;
    }

}
@Service
public class SomeService {

    @Resource(name = "someJsonString")
    private String someJsonString;

}
Mafor
  • 9,668
  • 2
  • 21
  • 36
  • This does not seem to work. I keep getting the following exception: `org.springframework.beans.factory.NoSuchBeanDefinitionException:No qualifying bean of type 'java.lan.String' available: expected at least 1 bean which qualifies as autowired candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true), @org.springframework.beans.factory.annotation.Qualifier(value=someJsonString)}` – ChikChak Jan 08 '20 at 21:51
  • @ChikChak Of course it works. Did you put the `JsonStringPropertyConfig` in the same package as the `SomeService`? – Mafor Jan 09 '20 at 12:04