Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conditionally Required Fields #582

Open
dwilkie opened this issue Aug 24, 2019 · 9 comments
Open

Conditionally Required Fields #582

dwilkie opened this issue Aug 24, 2019 · 9 comments

Comments

@dwilkie
Copy link

dwilkie commented Aug 24, 2019

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
Copy link
Member

solnic commented Aug 27, 2019

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 solnic transferred this issue from dry-rb/dry-schema Aug 27, 2019
@solnic solnic added the feature label Aug 27, 2019
@Mistgun
Copy link

Mistgun commented Aug 27, 2019

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

@bilby91
Copy link

bilby91 commented Nov 15, 2019

@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
Copy link
Member

solnic commented Dec 5, 2019

@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={}>

@jiggneshhgohel
Copy link

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
Copy link
Member

solnic commented Apr 29, 2020

@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
Copy link

@solnic Thanks for the prompt response.

@tonytonyjan
Copy link

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
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants