--- 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 ```