docs: def validate_schema(instance, schema, test_required=True, data_location=None,
                      skip_missing_data=False):
      """Check if DictField values are consistent with our data types.
  
      Perform basic JSON schema validation and our custom validations:
  
        * check that required fields are given (if `test_required` is set
          to ``True``)
        * check if ``basic:file:`` and ``list:basic:file`` fields match
          regex given in schema (only if ``validate_regex`` is defined in
          schema for coresponding fields) and exists (only if
          ``data_location`` is given)
        * check if directories referenced in ``basic:dir:`` and
          ``list:basic:dir``fields exist (only if ``data_location`` is
          given)
        * check that referenced ``Data`` objects (in ``data:<data_type>``
          and  ``list:data:<data_type>`` fields) exists and are of type
          ``<data_type>``
        * check that referenced ``Storage`` objects (in ``basic:json``
          fields) exists
  
      :param list instance: Instance to be validated
      :param list schema: Schema for validation
      :param bool test_required: Flag for testing if all required fields
          are present. It is usefule if validation is run before ``Data``
          object is finished and there are some field stil missing
          (default: ``False``)
      :param :class:`~resolwe.flow.models.data.DataLocation` data_location:
          data location used for checking if files and directories exist
          (default: ``None``)
      :param bool skip_missing_data: Don't raise an error if referenced
          ``Data`` object does not exist
      :rtype: None
      :raises ValidationError: if ``instance`` doesn't match schema
          defined in ``schema``
  
      """
      from .storage import Storage  # Prevent circular import.
  
      path_prefix = None
      if data_location:
          path_prefix = data_location.get_path()
  
      def validate_refs(field):
          """Validate reference paths."""
          for ref_filename in field.get('refs', []):
              ref_path = os.path.join(path_prefix, ref_filename)
              if not os.path.exists(ref_path):
                  raise ValidationError("Path referenced in `refs` ({}) does not exist.".format(ref_path))
              if not (os.path.isfile(ref_path) or os.path.isdir(ref_path)):
                  raise ValidationError(
                      "Path referenced in `refs` ({}) is neither a file or directory.".format(ref_path))
  
      def validate_file(field, regex):
          """Validate file name (and check that it exists)."""
          filename = field['file']
  
          if regex and not re.search(regex, filename):
              raise ValidationError(
                  "File name {} does not match regex {}".format(filename, regex))
  
          if path_prefix:
              path = os.path.join(path_prefix, filename)
              if not os.path.exists(path):
                  raise ValidationError("Referenced path ({}) does not exist.".format(path))
              if not os.path.isfile(path):
                  raise ValidationError("Referenced path ({}) is not a file.".format(path))
  
              validate_refs(field)
  
      def validate_dir(field):
          """Check that dirs and referenced files exists."""
          dirname = field['dir']
  
          if path_prefix:
              path = os.path.join(path_prefix, dirname)
              if not os.path.exists(path):
                  raise ValidationError("Referenced path ({}) does not exist.".format(path))
              if not os.path.isdir(path):
                  raise ValidationError("Referenced path ({}) is not a directory.".format(path))
  
              validate_refs(field)
  
      def validate_data(data_pk, type_):
          """Check that `Data` objects exist and is of right type."""
          from .data import Data  # prevent circular import
  
          data_qs = Data.objects.filter(pk=data_pk).values('process__type')
          if not data_qs.exists():
              if skip_missing_data:
                  return
  
              raise ValidationError(
                  "Referenced `Data` object does not exist (id:{})".format(data_pk))
          data = data_qs.first()
          if not data['process__type'].startswith(type_):
              raise ValidationError(
                  "Data object of type `{}` is required, but type `{}` is given. "
                  "(id:{})".format(type_, data['process__type'], data_pk))
  
      def validate_range(value, interval, name):
          """Check that given value is inside the specified range."""
          if not interval:
              return
  
          if value < interval[0] or value > interval[1]:
              raise ValidationError(
                  "Value of field '{}' is out of range. It should be between {} and {}.".format(
                      name, interval[0], interval[1]
                  )
              )
  
      is_dirty = False
      dirty_fields = []
      for _schema, _fields, _ in iterate_schema(instance, schema):
          name = _schema['name']
          is_required = _schema.get('required', True)
  
          if test_required and is_required and name not in _fields:
              is_dirty = True
              dirty_fields.append(name)
  
          if name in _fields:
              field = _fields[name]
              type_ = _schema.get('type', "")
  
              # Treat None as if the field is missing.
              if not is_required and field is None:
                  continue
  
              try:
                  jsonschema.validate([{"type": type_, "value": field}], TYPE_SCHEMA)
              except jsonschema.exceptions.ValidationError as ex:
                  raise ValidationError(ex.message)
  
              choices = [choice['value'] for choice in _schema.get('choices', [])]
              allow_custom_choice = _schema.get('allow_custom_choice', False)
              if choices and not allow_custom_choice and field not in choices:
                  raise ValidationError(
                      "Value of field '{}' must match one of predefined choices. "
                      "Current value: {}".format(name, field)
                  )
  
              if type_ == 'basic:file:':
                  validate_file(field, _schema.get('validate_regex'))
  
              elif type_ == 'list:basic:file:':
                  for obj in field:
                      validate_file(obj, _schema.get('validate_regex'))
  
              elif type_ == 'basic:dir:':
                  validate_dir(field)
  
              elif type_ == 'list:basic:dir:':
                  for obj in field:
                      validate_dir(obj)
  
              elif type_ == 'basic:json:' and not Storage.objects.filter(pk=field).exists():
                  raise ValidationError(
                      "Referenced `Storage` object does not exist (id:{})".format(field))
  
              elif type_.startswith('data:'):
                  validate_data(field, type_)
  
              elif type_.startswith('list:data:'):
                  for data_id in field:
                      validate_data(data_id, type_[5:])  # remove `list:` from type
  
              elif type_ == 'basic:integer:' or type_ == 'basic:decimal:':
                  validate_range(field, _schema.get('range'), name)
  
              elif type_ == 'list:basic:integer:' or type_ == 'list:basic:decimal:':
                  for obj in field:
                      validate_range(obj, _schema.get('range'), name)
  
      try:
          # Check that schema definitions exist for all fields
          for _, _ in iterate_fields(instance, schema):
              pass
      except KeyError as ex:
          raise ValidationError(str(ex))
  
      if is_dirty:
          dirty_fields = ['"{}"'.format(field) for field in dirty_fields]
          raise DirtyError("Required fields {} not given.".format(', '.join(dirty_fields)))