0

I have a PATCH REST endpoint, exposing a JSON interface, which can be used to partially update an entity, based on the attributes which are actually sent in the body.

Let's consider a sample class representing the entity:

class Account {
    private UUID accountUuid;
    private UUID ownerUuid;
    private String number;
    private String alias;

    // constructor, getters and setters omitted
}

where accountUuid, ownerUuid and number are required properties, and alias is optional. Additionally I have a command class for updating said account:

class UpdateAccountCommand {
    private UUID accountUuid;
    private String number;
    private String alias;

    // constructor, getters and setters omitted
}

For my PATCH endpoint, e.g. PATCH /accounts/{account-uuid}, I'd like to implement a functionality, that only properties actually sent in the request are changed:

// this should only update the account number
{"number": "123456"}

// this should only update the alias, by setting it to null
{"alias": null}

// this shouldn't update anything
{}

For required properties, this is fairly easy to do. After deserialisation from the JSON string to UpdateAccountCommand instance using Jackson, I simply check if a required property is null, and when it's not null, I update the given property.

However the situation complicates with optional properties, since the alias property is null in both cases:

  • when the request body explicitly specifies the alias as null,
  • when the alias property is not specified in the request body at all.

How can I model these optional properties, so that I can indicate this removable mechanism?

Andy
  • 1,127
  • 2
  • 12
  • 25
  • use a Map instead of DTO? – Adrian Jun 24 '20 at 11:09
  • @Adrian Not a very elegant solution, since the only map that would work would be `Map`, giving you no information about available properties (keys) as well as their types. Nothing would prevent anyone to e.g. pass an object in a place where you would expect a scalar value. – Andy Jun 24 '20 at 11:28

2 Answers2

1

A naive solution would be to introduce some sort of a wrapper, which would not only contain the raw value (e.g. for the alias: string property), but also a boolean, indicating whether a property was specified in the body or not. This would require you to write a custom deserialisation mechanism, which can be a tedious work.

Since the question is about Java 8, for Java 8 and newer, I recommend using a nullable Optional, which works pretty much out of the box with Jackson.

For optional (removable fields), you change the raw values by wrapping them in optional:

class UpdateAccountCommand {
    private UUID accountUuid;
    private String number;
    private Optional<String> alias;

    // constructor, getters and setters omitted
}

In order for Jackson to work with Optional<*> fields correctly, the Jdk8Module module has to be registered to the object mapper, e.g.:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new Jdk8Module());

The following code:

public static void main(String[] args) throws IOException {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new Jdk8Module());

    String withNewAliasS = "{\"alias\":\"New Alias\"}";
    String withNullAliasS = "{\"alias\":null}";
    String withoutPropertyS = "{}";

    UpdateAccountCommand withNewAlias = objectMapper.readValue(withNewAliasS, UpdateAccountCommand.class);
    if (withNewAlias.getAlias() != null && withNewAlias.getAlias().isPresent()) {
        System.out.println("withNewAlias specified new alias.");
    }

    UpdateAccountCommand withNullAlias = objectMapper.readValue(withNullAliasS, UpdateAccountCommand.class);
    if (withNullAlias.getAlias() != null && !withNullAlias.getAlias().isPresent()) {
        System.out.println("withNullAlias will remove an alias.");
    }

    UpdateAccountCommand withoutProperty = objectMapper.readValue(withoutPropertyS, UpdateAccountCommand.class);
    if (withoutProperty.getAlias() == null) {
        System.out.println("withoutProperty did not contain alias property on input at all.");
    }
}

then prints out this to the console:

withNewAlias specified new alias.
withNullAlias will remove an alias.
withoutProperty did not contain alias property on input at all.
Andy
  • 1,127
  • 2
  • 12
  • 25
  • [having an Optional in a class field or in a data structure, is considered a misuse of the API](https://stackoverflow.com/a/23464794/8198056) – Adrian Jun 24 '20 at 11:36
0

you can add an additional boolean property which says if the optional property was present in request

class UpdateAccountCommand {
  //...
  private String alias;
  @JsonIgnore
  private boolean isAliasSet;

  @JsonProperty
  public void setAlias(String value) {
     this.alias = value;
     this.isAliasSet = true;
  }
}

the setter is called only when "alias" is present, be it null or with value

Adrian
  • 2,984
  • 15
  • 27
  • 1
    I don't think this is much of an improvement. It unfortunately leads to exactly what I am trying to avoid, writing unnecessary code, basically polluting a class and making it twice as large for optional fields. To fix this, a wrapper would then need to be introduced (as I am mentioning in my own answer), you need to provide custom deserialisation mechanism, and how is then having a `CustomOptional` better than `Optional`, when the latter provides the desired mechanism pretty much out of the box? – Andy Jun 24 '20 at 11:45