dry-rb / dry-validation

Validation library with type-safe schemas and rules
https://dry-rb.org/gems/dry-validation
MIT License
1.34k stars 189 forks source link

Conditionally Required Fields #582

Open dwilkie opened 5 years ago

dwilkie commented 5 years ago

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.

solnic commented 5 years ago

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.

Mistgun commented 5 years ago

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

bilby91 commented 5 years ago

@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.

solnic commented 4 years ago

@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: 'jane@doe.org')
#<Dry::Validation::Result{:name=>"Jane", :email=>"jane@doe.org"} errors={}>

update_contract = UserContract::Update.new

update_contract.(email: 'jane@doe.org')
#<Dry::Validation::Result{:email=>"jane@doe.org"} errors={}>
jiggneshhgohel commented 4 years ago

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.

solnic commented 4 years ago

@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.

jiggneshhgohel commented 4 years ago

@solnic Thanks for the prompt response.

tonytonyjan commented 1 year ago

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"],
      },
    ],
}
flash-gordon commented 1 year ago

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.