7

I have two fields: startDate and endDate and I need to make sure the end date is equal to or later than the start date. What's the best way to do this?

I would like to ensure that endDate is deserialized after startDate, so I can put the logic in its setter method like:

@JsonSetter( "end" )
public void setEnd(String end)
{
    this.endDate = parseZonedDateTime( end );

    // Invalid
    if ( this.endDate.compareTo( this.startDate ) < 0 )
    {
        // Throw a validation exception
    }
}

But that only works if start is guaranteed to be set first.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Don Rhummy
  • 24,730
  • 42
  • 175
  • 330
  • why not write a custom deserializer? – best wishes Mar 26 '18 at 03:09
  • 1
    Possible duplicate of [Jackson ObjectMapper - specify serialization order of object properties](https://stackoverflow.com/questions/27577701/jackson-objectmapper-specify-serialization-order-of-object-properties) – Alexander Polozov Mar 26 '18 at 03:19
  • 1
    Add a constructor that takes all your fields and do validation there. See [this question](https://stackoverflow.com/questions/21920367/why-when-a-constructor-is-annotated-with-jsoncreator-its-arguments-must-be-ann) for notes on constructor annotation. – teppic Mar 26 '18 at 03:27
  • May not be a clean solution but kind of a hack is to put a placeholder and inject or replace it at the end ? – royalghost Mar 26 '18 at 03:40
  • 3
    @AlexanderPolozov that's serialization, not deserialization – Don Rhummy Mar 26 '18 at 16:56

6 Answers6

7

I have two fields: startDate and endDate and I need to make sure the end date is equal to or later than the start date. What's the best way to do this?

I would not try to do this by jackson. Jackson should only focus on converting json to object. The valid of values should be taken care of by jackson. Nor the deserialization order.

Try validating after jackson's converting, either manually or by validation framework like JSR-303.

John
  • 1,654
  • 1
  • 14
  • 21
  • and how would i do that when there's no post construct in Jackson – Don Rhummy Mar 26 '18 at 16:55
  • 1
    I would prefer `Result result = objectMapper.readValue(json, Result.class); result.validat()` – John Mar 28 '18 at 02:32
  • i can't do that. this is spring. – Don Rhummy Mar 28 '18 at 02:33
  • 1
    http://www.bbenson.co/post/spring-validations-with-examples/ shows a way of spring validation – John Mar 28 '18 at 02:39
  • @DonRhummy what do you mean "this is spring"? You never get handle the deserialized object? – daniu Jul 16 '23 at 18:00
  • This is the correct answer. Validation is *not* Jackson's job. Spring can do it after deserializing. – Jorn Jul 17 '23 at 13:16
  • I assume, "_The valid of values should be taken care of by jackson_", should actually be, "_The valid**ity** of values should **not** be taken care of by jackson_"? – Slaw Jul 22 '23 at 20:56
1

In your case, a better approach would be to perform the validation after all properties have been set, not inside the setter. You can use the @JsonCreator and @JsonProperty annotations to achieve this:

public class DateRange {

    private ZonedDateTime startDate;
    private ZonedDateTime endDate;

    @JsonCreator
    public DateRange(@JsonProperty("start") String start, @JsonProperty("end") String end) {
        this.startDate = parseZonedDateTime(start);
        this.endDate = parseZonedDateTime(end);

        if (this.endDate.compareTo(this.startDate) < 0) {
            throw new IllegalArgumentException("End date must be after start date");
        }
    }
}
Haifisch
  • 21
  • 1
0

I'd suggest to create a custom deserializer and register it in your object mapper for this particular class (say MyDateObject). Assuming MyDateObject has two fields - startDate & endDate, you can impose deserializing startDate before endDate using something like this:

public class CustomDeserializer extends JsonDeserializer<MyDateObject> {
    @Override
    public MyDateObject deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        long startDate = 0;
        long endDate = 0;
        while (!jsonParser.isClosed()) {
            String fieldName = jsonParser.nextFieldName();
            if (fieldName == null) {
                break;
            }

            // Check the field name and assign values
            // to the corresponding fields
            switch (fieldName) {
                case "startDate":
                    startDate = jsonParser.nextLongValue(0L);
                    break;

                case "endDate":
                    endDate = jsonParser.nextLongValue(0L);
                    break;

                default:
                    // If you have other fields in the JSON that
                    // you want to ignore, you can skip them.
                    jsonParser.skipChildren();
                    break;
            }
        }
        return generateNewMyDateObject(startDate, endDate);
    }

    private MyDateObject generateNewMyDateObject(long startDate, long endDate) {
        MyDateObject myDate = new MyDateObject();
        myDate.setStartDate(startDate);
        myDate.setEndDate(endDate);
        return myDate;
    }
}

Of course the code can be cleaner, but I'll leave it to you as the business expert. Basically, we keep the two values from the JSON content, and only after we got both of them, we generate the MyDateObject, with startDate first. Such that you can implement in the setter of endDate whatever you want, and you can assume startDate already has a value.

Then, you can register this custom deserializer to your object mapper:

ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(MyDateObject.class, new CustomDeserializer());
objectMapper.registerModule(module);

And use this object mapper for deserialization:

String jsonString = "{\"endDate\":123,\"startDate\":30}";
MyDateObject customObject = objectMapper.readValue(jsonString, MyDateObject.class);

Note: If you're using Spring Boot, it's even easier. Just define this object mapper as a Bean in your @Configuration class, and let Spring use it for deserialization automatically.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
idanz
  • 821
  • 1
  • 6
  • 17
-1

You have to use the annotation @JsonPropertyOrder({"startDate", "endDate"}). The annotation can be used to define ordering (possibly partial) to use when serializing object properties.

@JsonPropertyOrder({"startDate", "endDate"})
public class MyClass {

    private ZonedDateTime startDate;
    private ZonedDateTime endDate;

    @JsonSetter("startDate")
    public void setStartDate(String startDate) {
        this.startDate = parseZonedDateTime(startDate);
    }

    @JsonSetter("endDate")
    public void setEndDate(String end) {
        
        this.endDate = parseZonedDateTime( end );
        if (this.startDate == null) {
            throw new IllegalStateException("startDate must be set before endDate");
        }
        //invalid
        if ( this.endDate.compareTo( this.startDate ) < 0 )
        {
            //Throw a validation exception
        }
    }
}
-2

@Hitobat suggests to annotate constructor arguments with JsonCreator instead of trying to annotate fields.

Constructs body can then contain validation logic.

Basilevs
  • 22,440
  • 15
  • 57
  • 102
  • 2
    Just because you *can* do this, doesn't mean you *should*. @John's answer explains why. – Jorn Jul 17 '23 at 13:14
  • 1
    @Jorn I disagree. There are various levels of validation and transport level should produce a valid object (not business valid, but transport valid). Validation in constructor is a good practice and makes creation of invalid object impossible. – Basilevs Jul 17 '23 at 19:06
-2

JsonPropertyOrder should work for the ordering field and we can have the logic of validation in validate() This would be my approach, hope it will be useful for others.

import java.time.ZonedDateTime; import com.fasterxml.jackson.annotation.JsonPropertyOrder;

@JsonPropertyOrder({ "startDate", "endDate" })
public class Result {
    private ZonedDateTime startDate;
    private ZonedDateTime endDate;

    // Getters and setters for startDate and endDate

    public void setStartDate(ZonedDateTime startDate) {
        this.startDate = startDate;
    }

    public void setEndDate(ZonedDateTime endDate) {
        this.endDate = endDate;
    }

    // Validation method to be called after deserialization
    public void validate() {
        if (endDate != null && startDate != null && endDate.isBefore(startDate)) {
            throw new IllegalArgumentException("End date cannot be before the start date.");
        }
    }
}
Gautam
  • 3,707
  • 5
  • 36
  • 57