1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
|
---
title: Hash Schemas
layout: gem-single
name: dry-types
---
It is possible to define a type for a hash with a known set of keys and corresponding value types. Let's say you want to describe a hash containing the name and the age of a user:
```ruby
# using simple kernel coercions
user_hash = Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
user_hash[name: 'Jane', age: '21']
# => { name: 'Jane', age: 21 }
# :name left untouched and :age was coerced to Integer
```
If a value doesn't conform to the type, an error is raised:
```ruby
user_hash[name: :Jane, age: '21']
# => Dry::Types::SchemaError: :Jane (Symbol) has invalid type
# for :name violates constraints (type?(String, :Jane) failed)
```
All keys are required by default:
```ruby
user_hash[name: 'Jane']
# => Dry::Types::MissingKeyError: :age is missing in Hash input
```
Extra keys are omitted by default:
```ruby
user_hash[name: 'Jane', age: '21', city: 'London']
# => { name: 'Jane', age: 21 }
```
### Default values
Default types are **only** evaluated if the corresponding key is missing in the input:
```ruby
user_hash = Types::Hash.schema(
name: Types::String,
age: Types::Integer.default(18)
)
user_hash[name: 'Jane']
# => { name: 'Jane', age: 18 }
# nil violates the constraint
user_hash[name: 'Jane', age: nil]
# => Dry::Types::SchemaError: nil (NilClass) has invalid type
# for :age violates constraints (type?(Integer, nil) failed)
```
In order to evaluate default types on `nil`, wrap your type with a constructor and map `nil` to `Dry::Types::Undefined`:
```ruby
user_hash = Types::Hash.schema(
name: Types::String,
age: Types::Integer.
default(18).
constructor { |value|
value.nil? ? Dry::Types::Undefined : value
}
)
user_hash[name: 'Jane', age: nil]
# => { name: 'Jane', age: 18 }
```
The process of converting types to constructors like that can be automated, see "Type transformations" below.
### Optional keys
By default, all keys are required to present in the input. You can mark a key as optional by adding `?` to its name:
```ruby
user_hash = Types::Hash.schema(name: Types::String, age?: Types::Integer)
user_hash[name: 'Jane']
# => { name: 'Jane' }
```
### Extra keys
All keys not declared in the schema are silently ignored. This behavior can be changed by calling `.strict` on the schema:
```ruby
user_hash = Types::Hash.schema(name: Types::String).strict
user_hash[name: 'Jane', age: 21]
# => Dry::Types::UnknownKeysError: unexpected keys [:age] in Hash input
```
### Transforming input keys
Keys are supposed to be symbols but you can attach a key tranformation to a schema, e.g. for converting strings into symbols:
```ruby
user_hash = Types::Hash.schema(name: Types::String).with_key_transform(&:to_sym)
user_hash['name' => 'Jane']
# => { name: 'Jane' }
```
### Inheritance
Hash schemas can be inherited in a sense you can define a new schema based on an existing one. Declared keys will be merged, key and type transformations will be preserved. The `strict` option is also passed to the new schema if present.
```ruby
# Building an empty base schema
StrictSymbolizingHash = Types::Hash.schema({}).strict.with_key_transform(&:to_sym)
user_hash = StrictSymbolizingHash.schema(
name: Types::String
)
user_hash['name' => 'Jane']
# => { name: 'Jane' }
user_hash['name' => 'Jane', 'city' => 'London']
# => Dry::Types::UnknownKeysError: unexpected keys [:city] in Hash input
```
### Transforming types
A schema can transform types with a block. For example, the following code makes all keys optional:
```ruby
user_hash = Types::Hash.with_type_transform { |type| type.required(false) }.schema(
name: Types::String,
age: Types::Integer
)
user_hash[name: 'Jane']
# => { name: 'Jane' }
user_hash[{}]
# => {}
```
Type transformations work perfectly with inheritance, you don't have to define same rules more than once:
```ruby
SymbolizeAndOptionalSchema = Types::Hash
.schema({})
.with_key_transform(&:to_sym)
.with_type_transform { |type| type.required(false) }
user_hash = SymbolizeAndOptionalSchema.schema(
name: Types::String,
age: Types::Integer
)
user_hash['name' => 'Jane']
```
You can check key name by calling `.name` on the type argument:
```ruby
Types::Hash.with_type_transform do |key|
if key.name.to_s.end_with?('_at')
key.constructor { |v| Time.iso8601(v) }
else
key
end
end
```
|