5

I've built a REST endpoint using Spring Boot. JSON is posted to the endpoint. Jackson converts the JSON giving me an object.

The JSON look like this:

{
    "parameterDateUnadjusted": "2017-01-01",
    "parameterDateAdjusted": "2017-01-02"
}

Jackson converts the JSON to an object based on this class:

public class ParameterDate {

    @NotNull(message = "Parameter Date Unadjusted can not be blank or null")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date parameterDateUnadjusted;
    @NotNull(message = "Parameter Date Adjusted can not be blank or null")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date parameterDateAdjusted;
    private Date parameterDateAdded;
    private Date parameterDateChanged;
}

This all works fine. The issue I'm having is that I would like to validate the data before Jackson converts the data. For instance if I post

{
    "parameterDateUnadjusted": "2017-01-01",
    "parameterDateAdjusted": "2017-01-40"
}

Where parameterDateAdjusted is not a valid date (there is no month with 40 days in it). Jackson converts this to 2017-02-09. One way of getting around this is to have a class that is only strings let's call it ParameterDateInput. Validate each filed with Hibernate Validator in the parameterDateInput object and then copy the parameterDateInput object to parameterDate where each field has the correct type (dates are of type Date and not of type String). This to me doesn't look like a very elegant solution. Is there some other way I can solve this? How is data generally validated in Spring Boot when posted as JSON? I like to be able to send back a message to the user/client what is wrong with the data that is being posted.

g3blv
  • 3,877
  • 7
  • 38
  • 51
  • Possible duplicate of [How do I validate incoming JSON data inside a REST service?](https://stackoverflow.com/questions/18154983/how-do-i-validate-incoming-json-data-inside-a-rest-service) – K.Nicholas Jun 06 '17 at 14:20

2 Answers2

1

How about a custom JSON deserializer where you can write down the logic you want:

@RestController
public class JacksonCustomDesRestEndpoint {

    @RequestMapping(value = "/yourEndPoint", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public Object createRole(@RequestBody ParameterDate paramDate) {
        return paramDate;
    }
}

@JsonDeserialize(using = RoleDeserializer.class)
public class ParameterDate {
    // ......
}

public class RoleDeserializer extends JsonDeserializer<ParameterDate> {
    @Override
    public ParameterDate deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        ObjectCodec oc = jsonParser.getCodec();
        JsonNode node = oc.readTree(jsonParser);
        String parameterDateUnadjusted = node.get("parameterDateUnadjusted").getTextValue();
        //Do what you want with the date and set it to object from type ParameterDate and return the object at the end. 
        //Don't forget to fill all the properties to this object because you do not want to lose data that came from the request.
        return something;
    }
}
Lazar Lazarov
  • 2,412
  • 4
  • 26
  • 35
  • Do you have any example of what the contents of the `deserialize` method would look like? – g3blv Jun 06 '17 at 14:56
0

There is a way to check the dates. setLenient() method

public static boolean isValidDate(String inDate, String format) {
    SimpleDateFormat dateFormat = new SimpleDateFormat(format);
    dateFormat.setLenient(false);
    try {
      dateFormat.parse(inDate.trim());
    } catch (ParseException pe) {
      return false;
    }
    return true;
 }

Just define own annotation to validate the value

@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = MyDateFormatCheckValidator.class)
@Documented
public @interface MyDateFormatCheck {
    String pattern();
...

and the validator class

public class MyDateFormatCheckValidator implements ConstraintValidator<MyDateFormatCheck, String> {

    private MyDateFormatCheck check; 
    @Override
    public void initialize(MyDateFormatCheck constraintAnnotation) {
        this.check= constraintAnnotation;
    }

    @Override
    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
        if ( object == null ) {
            return true;
        }

        return isValidDate(object, check.pattern());
    }
}
StanislavL
  • 56,971
  • 9
  • 68
  • 98
  • I've create an interface called MyDateFormatCheck and a class called MyDateFormatCheckValidator that implements the interface. In the class I put the isValidDate method. The I annotated my fields like @MyDateFormatCheck(pattern = "yyyy-MM-dd", message = "Not matching") private Date parameterDateUnadjusted; @MyDateFormatCheck(pattern = "yyyy-MM-dd", message = "Not matching") private Date parameterDateAdjusted;. I added message since it gave me an error. Now I get another error MyDateFormatCheck contains Constraint annotation, but does not contain a groups parameter.". – g3blv Jun 06 '17 at 14:08
  • Mostly this way but MyDateFormatCheckValidator does not implement the interface but ConstraintValidator instead. See the example http://www.journaldev.com/3626/hibernate-validator-jsr303-example-tutorial – StanislavL Jun 06 '17 at 14:11
  • I sort of got this working but I needed to change the field type for `parameterDateUadjusted` and `parameterDateAdjusted` to String which is not what I want since I want Jackson to convert the `JSON` values to data of type `Date`. – g3blv Jun 06 '17 at 14:50
  • Validate can happen only **after** converting but if the sent date has wrong format instead of validation message you have converter exception (you can try old way and pass e.g `abc` rather than date). I see no good solution for the problem except using strings. You can add getter/setter which converts the string to Date on fly. – StanislavL Jun 06 '17 at 15:00