Separating `nil` from value never given in dry-struct

:wave: friends TGIF!

I am wondering if anyone has solved the following problem as I have failed to do so both in dry-* and sorbet. I’d like to name this problem the “PATCH problem” :tm:

Let’s assume we have the following a users table with
name → not nullable string
dob → (date of birth) nullable string
gender → nullable string

In a typical PATCH request the user gets to update whatever the provided in the request body only, therefore we should not update fields that are missing.

The loosely typed nature of hashes has an advantage over types as an input: there is a clear distinction between a missing input and a null input. Let’s see an example.

(1) The following request body is relatively clear, we want to update the two fields.

{"user": {"name":  "Meredith", "dob":  "1968-01-01", "gender": "Female"}}

(2) With this body, we just want to nullify the dob.

{"user": {"dob": null}}

(3) With this body, we just want to update the name

{"user": {"name":  "Dwight"}}

Let’s represent the above as a type

class User < Dry::Struct
  attribute :name, String
  attribute :dob, Types::String.optional
  attribute :gender, Types::String.optional
end

And now we have an issue at hand. Looking at an instance of a user, how do you know dob is null because scenario 2 or 3? You cannot really distinguish.

It might be worth mentioning that I want to craft a domain type or even a typed input from the controller before I pass it in the service / domain layer.

In Haskell or Elm for instance, the above can be represented as

attribute :dob, Unchanged | Deleted | Updated a

I wonder if anyone has come across this before and has a workable solution. For instance, a simple but not great implementation would be

class User < Dry::Struct
  attribute :name, String
  attribute :dob, Types::String.optional
  attribute :dob_given?, Bool
  # ...
  attribute :gender, Types::String.optional
end

Another example is having a specific struct for each possibility(Expressing the type of attribute B as dependent on an arbitrary value of attribute A.) but that can explode relatively quickly.

where we explicitly ask the crafter of the Struct to tell us if the input is actually nil because the user provided nil or never given.

Bonus points: one extra worry I have is how would any solution play with conversion to other types like my_struct.to_hash.