dry-validation icon indicating copy to clipboard operation
dry-validation copied to clipboard

Conditionally Required Fields

Open dwilkie opened this issue 5 years ago • 9 comments

In many REST APIs it's often the case when creating resources, certain fields are required but when updating resources, all fields are optional and only the fields which are provided are updated.

Examples

For example, the following schema is good for validating user creation.

  params do
    required(:name).filled(str?)
    required(:phone_number).filled(:str?)
    optional(:metadata).maybe(:hash?)
    required(:additional_details).filled(:hash?).schema do
      required(:name_km).filled(:str?)
      required(:date_of_birth).value(:date, :filled?)
    end
  end

But when updating a user, we need the following schema:

  params do
    optional(:name).filled(str?)
    optional(:phone_number).filled(:str?)
    optional(:metadata).maybe(:hash?)
    optional(:additional_details).filled(:hash?).schema do
      optional(:name_km).filled(:str?)
      optional(:date_of_birth).value(:date, :filled?)
    end
  end

We don't want to repeat the schema and the rules

My first thought of a possible solution would be something like:

  option :resource, optional: true

  params do
    conditionally_required(:name).filled(str?) { resource.present? }
    conditionally_required(:phone_number).filled(:str?) { resource.present? }
    conditionally_required(:metadata).maybe(:hash?) { resource.present? }
    conditionally_required(:additional_details).filled(:hash?) { resource.present? }.schema do
      conditionally_required(:name_km).filled(:str?)  { resource.present? }
      conditionally_required(:date_of_birth).value(:date, :filled?)  { resource.present? }
    end
  end

For a PATCH request, the resource could be injected as an external dependency, for a POST request the resource doesn't exist yet so it's not injected.

dwilkie avatar Aug 24 '19 06:08 dwilkie

I'm not entirely sure how to solve this yet, but what I am sure is that this is definitely not something that will require extending the schema DSL. Maybe we could have a mechanism for converting existing schema from required keys to optional.

Anyway, this is something that dry-validation can solve eventually, so I'm moving this issue there.

solnic avatar Aug 27 '19 02:08 solnic

@dwilkie you could just send all of those params again, instead of sending blank values on update.

Mistgun avatar Aug 27 '19 10:08 Mistgun

@solnic Do you have any ideas on how dry-validation can solve this ? Something like having variants of a schema ? Or having another api to transform the main schema ? We are needing this feature as well in order to not duplicate contracts that have different schemas.

bilby91 avatar Nov 15 '19 16:11 bilby91

@bilby91 I don't have. For the time being you can simply do this:

require 'dry-validation'

class UserContract < Dry::Validation::Contract
  def self.define_params(key = :required)
    params do
      public_send(key, :name).value(:string)
      public_send(key, :email).value(:string)
    end
  end
end

class UserContract::Create < UserContract
  define_params
end

class UserContract::Update < UserContract
  define_params(:optional)
end

create_contract = UserContract::Create.new

create_contract.(name: 'Jane', email: '[email protected]')
#<Dry::Validation::Result{:name=>"Jane", :email=>"[email protected]"} errors={}>

update_contract = UserContract::Update.new

update_contract.(email: '[email protected]')
#<Dry::Validation::Result{:email=>"[email protected]"} errors={}>

solnic avatar Dec 05 '19 09:12 solnic

I had a related query to this issue which I was about to ask and thought let me first check if I can find any related issues in open issues list and I ended up finding this. My use-case is

I have following schema for an Address model I have:

  ValidationContexts = Types::String.enum('abc', 'def')

  params do
      required(:address_line_1).filled(Types::StrippedString)
      optional(:address_line_2).maybe(Types::StrippedString)
      required(:city).filled(Types::StrippedString)
      required(:state).filled(Types::StrippedString)
      required(:country).filled(Types::StrippedString)
      required(:zip_code).filled(Types::StrippedString)

      required(:contact_detail).filled(Types::Instance(::ContactDetail))

      optional(:validation_context).filled(ValidationContexts)
   end

I have a need wherein applying a rule on optional validation_context I want to make the required contact_detail optional.

rule(:validation_context, :contact_detail) do
  if key?
    case values[:validation_context]
      when ValidationContexts['abc']
         # make contact_detail optional. Is this possible?
      else
        # do nothing
    end
  end
end

Is something like that possible?

My setup

ruby '2.7.1'
gem 'rails', '~> 6.0.2', '>= 6.0.2.2'
gem 'dry-validation', '~> 1.5'

Thanks.

jiggneshhgohel avatar Apr 29 '20 10:04 jiggneshhgohel

@jiggneshhgohel no it's not possible. Once you're within a rule your schema is already established so you can't alter it based on input.

solnic avatar Apr 29 '20 11:04 solnic

@solnic Thanks for the prompt response.

jiggneshhgohel avatar Apr 29 '20 11:04 jiggneshhgohel

I am having the same issue. What I am trying to do is:

  1. params[:foo] is required.
  2. If params[:foo] is true, then params[:bar] is required, otherwise params[:bar] is optional.

Looks like it's not possible using dry-validation.

The equivalent JSON schema looks like:

{
  "anyOf":
    [
      {
        "title": "foo is true",
        "properties": { "foo": { "type": "boolean", "enum": [true] } },
      },
      {
        "title": "foo is false",
        "properties":
          {
            "foo": { "type": "boolean", "enum": [false] },
            "bar": { "type": "string"},
          },
        "required": ["bar"],
      },
    ],
}

tonytonyjan avatar Nov 19 '22 09:11 tonytonyjan

Looks like it's not possible using dry-validation.

Dependencies between fields must be handled via rules, not through the schema dsl. :bar should always be optional, then you check its presence with custom logic in a rule block.

flash-gordon avatar Nov 21 '22 08:11 flash-gordon