# dry-schema: split string, validate elements against enum

**URL:** <https://discourse.dry-rb.org/t/dry-schema-split-string-validate-elements-against-enum/1777>\
**Category:** Support\
**Created:** [February 12, 2024, 12:47pm UTC](https://discourse.dry-rb.org/t/dry-schema-split-string-validate-elements-against-enum/1777 "2024-02-12T12:47:49Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![dmke](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.dry-rb.org/dmke/32/951_2.png) [@dmke](https://discourse.dry-rb.org/u/dmke)\
**Post date:** [February 12, 2024, 12:47pm UTC](https://discourse.dry-rb.org/t/dry-schema-split-string-validate-elements-against-enum/1777/1 "2024-02-12T12:47:49Z")

</div>

Hi,

I get a parameter named `includes` as a comma-separated string, and want to validate that all its elements are within a set of allowed values. I believe, this is conceptually similar to [this question](https://discourse.dry-rb.org/t/how-can-i-convert-a-string-into-an-array-of-hashes/819), where an input string is coerced into an array of hashes.

I feel like I’m close, but something’s missing:

```ruby
require "dry-schema"

module T
  include Dry.Types()

  CommaSepString = T.Constructor(Array) do |values|
    values.split(",")
  end
end

pp T::CommaSepString.("a,b,c")
#=> ["a", "b", "c"]

schema = Dry::Schema.Params do
  optional(:includes).value(T::CommaSepString, included_in?: %w[users favorites])
end

pp schema.("includes" => "users,favorites")
#=> #<Dry::Schema::Result{:includes=>["users", "favorites"]} errors={:includes=>["must be one of: users, favorites"]} path=[]>

```

I’ve also tried the `array` macro:

```ruby
schema = Dry::Schema.Params do
  optional(:includes).array(T::CommaSepString, included_in?: %w[users favorites])
end

pp schema.("includes" => "users,favorites")
# #<Dry::Schema::Result{:includes=>"users,favorites"} errors={:includes=>["must be an array"]} path=[]>

```

---

<div class="post-metadata">

**Author:** ![alassek](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.dry-rb.org/alassek/32/314_2.png) [@alassek](https://discourse.dry-rb.org/u/alassek)\
**Post date:** [February 17, 2024, 10:40pm UTC](https://discourse.dry-rb.org/t/dry-schema-split-string-validate-elements-against-enum/1777/2 "2024-02-17T22:40:59Z")

</div>

Your problem is that you’re applying the constraint against the whole array rather than the individual elements.

Here how I solved a very similar problem

```ruby
T = Dry.Types()

module T
  Inclusion = String.enum('users', 'favorites')

  module Params
    CSVList = T::Array.of(String).constructor do |value|
      if value.is_a?(::String)
        value.split(',')
      else
        Array(value)
      end
    end
  end
end

schema = Dry::Schema.Params do
  optional(:includes).value(T::Params::CSVList.of(T::Inclusion))
end

```

```auto
[2] pry(main)> schema.({ includes: 'users,favorites' })
=> #<Dry::Schema::Result{:includes=>["users", "favorites"]} errors={} path=[]>
[3] pry(main)> schema.({ includes: 'users,favorites,whatever' })
=> #<Dry::Schema::Result{:includes=>["users", "favorites", "whatever"]} errors={:includes=>{2=>["must be one of: users, favorites"]}} path=[]>
[4] pry(main)> 

```

---

<div class="post-metadata">

**Author:** ![dmke](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.dry-rb.org/dmke/32/951_2.png) [@dmke](https://discourse.dry-rb.org/u/dmke)\
**Post date:** [March 4, 2024, 11:31am UTC](https://discourse.dry-rb.org/t/dry-schema-split-string-validate-elements-against-enum/1777/3 "2024-03-04T11:31:31Z")

</div>

That works perfectly, almost 🙂

One thing that slipped my mind is, that I don’t use

```ruby
T = Dry.Types()

```

but

```ruby
T = Dry.Types(default: :params)

```

> **Full code sample**
>
> ```ruby
> module T
> include Dry.Types(default: :params)
> 
> CSVList = T::Array.of(String).constructor do |value|
> if value.is_a?(::String)
> value.split(',')
> else
> Array(value)
> end
> end
> end
> 
> module Lists
> IncludeValues = T::String.enum('users', 'favorites')
> 
> IncludeParams = Dry::Schema.Params do
> optional(:includes).value(T::CSVList.of IncludeValues)
> end
> end
> 
> pp Lists::IncludeParams.('includes' => 'users,favorites')
> 
> ```

This still yields an error:

```ruby
#<Dry::Schema::Result{:includes=>"users,favorites"} errors={:includes=>["must be an array"]} path=[]>

```

---

<div class="post-metadata">

**Author:** ![alassek](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.dry-rb.org/alassek/32/314_2.png) [@alassek](https://discourse.dry-rb.org/u/alassek)\
**Post date:** [March 4, 2024, 6:59pm UTC](https://discourse.dry-rb.org/t/dry-schema-split-string-validate-elements-against-enum/1777/4 "2024-03-04T18:59:27Z")

</div>

It’s because `T::Params` is trying to do its coercion logic before the custom constructor runs. Types can have multiple constructors, and by default you’re adding to the end of the list.

You can work around this by prepending it

```ruby
CSVList = T::Array.of(String).prepend do |value|
  if value.is_a?(::String)
    value.split(',')
  else
    Array(value)
  end
end

```

But I recommend using `T::Strict::Array` instead, which is more intention-revealing.

```ruby
CSVList = T::Strict::Array.of(String).constructor do |value|
  if value.is_a?(::String)
    value.split(',')
  else
    Array(value)
  end
end

```

I would recommend not using `include`, even though this is how it is currently demonstrated on the website. There was a discussion about this somewhere.

The downside of `include` is that if you open your own `Params` module etc you will replace the original, whereas with this form:

```ruby
T = Dry.Types(default: :params)

module T
  module Params
    CSVList = T::Array.of(String).prepend do |value|
      if value.is_a?(::String)
        value.split(',')
      else
        Array(value)
      end
    end
  end
end

```

`Params` reopens the Dry module. I do this because `Strict`, `Params` etc is useful information about how a type is intended to be used.

---

<div class="post-metadata">

**Author:** ![dmke](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.dry-rb.org/dmke/32/951_2.png) [@dmke](https://discourse.dry-rb.org/u/dmke)\
**Post date:** [March 5, 2024, 10:01am UTC](https://discourse.dry-rb.org/t/dry-schema-split-string-validate-elements-against-enum/1777/5 "2024-03-05T10:01:43Z")

</div>

> Types can have multiple constructors, and by default you’re adding to the end of the list.

I wasn’t aware of that.

> I would recommend not using `include`, even though this is how it is currently demonstrated on the website. There was a discussion about this somewhere.
> 
> The downside of `include` is that if you open your own `Params` module etc you will replace the original

Now that you’re mentioning it, I do have a `Params` module 🙂

(For future readers: On this topic I found [#422 (Params namespace disapearing)](https://github.com/dry-rb/dry-types/issues/422) opened in 2021 and [pr#432 (Update docs to recommend upfront module definition (i.e. `Types = Dry.Types()`))](https://github.com/dry-rb/dry-types/pull/432) from January 2022.)

> I recommend using `T::Strict::Array`

That has worked, thank you very much!

---

<div class="post-metadata">

**Author:** ![alassek](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.dry-rb.org/alassek/32/314_2.png) [@alassek](https://discourse.dry-rb.org/u/alassek)\
**Post date:** [March 6, 2024, 5:52pm UTC](https://discourse.dry-rb.org/t/dry-schema-split-string-validate-elements-against-enum/1777/6 "2024-03-06T17:52:58Z")

</div>

> [@alassek](#):
>
> Types can have multiple constructors, and by default you’re adding to the end of the list.

I should say, for completeness: this is not _quite_ accurate. It’s not maintaining a list of constructors so much as composing them together.

So the difference between `constructor` and `prepend` is like `f ∘ g` vs `g ∘ f`
