text: def make_collection(collection_type, element_type, allow_empty_data=False):
      class DynamicCollection(collection_type):
          type = element_type
          allow_empty = allow_empty_data
  
      return DynamicCollection
  
  
  def as_data(value):
      if hasattr(value, 'as_data'):
          return value.as_data()
      else:
          return value
  
  
  class AbstractValue:
      @classmethod
      def from_data(cls, data):
          raise NotImplementedError
  
      def as_data(self):
          raise NotImplementedError
  
      def __eq__(self, other):
          try:
              return self.as_data() == other.as_data()
          except AttributeError:
              return False
  
      def __repr__(self):
          return '{}.from_data({})'.format(type(self).__name__, self.as_data())
  
      @classmethod
      def allow_as_data(cls, data):
          return bool(data)
  
  
  class Scalar(AbstractValue):
      def __init__(self, data):
          self.data = data
  
      @classmethod
      def from_data(cls, data):
          return data
  
      def as_data(self):
          return self.data
  
  
  class AbstractCollectionValue(AbstractValue):
      type = Scalar
      allow_empty = False
  
      @classmethod
      def allow_as_data(cls, value):
          return bool(value) or cls.allow_empty
  
  
  class List(AbstractCollectionValue, list):
      @classmethod
      def from_data(cls, data):
          if data is None:
              return cls()
          if not hasattr(data, '__iter__'):
              raise CantLoadData
          return cls([cls.type.from_data(element) for element in data])
  
      def as_data(self):
          return [as_data(element) for element in self]
  
  
  class Dictionary(AbstractCollectionValue, dict):
      @classmethod
      def from_data(cls, data):
          if data is None:
              return cls()
          if not hasattr(data, 'items'):
              raise CantLoadData
          return cls({key: cls.type.from_data(element) for key, element in data.items()})
  
      def as_data(self):
          return {key: as_data(element) for key, element in self.items()}
  
  
  class Record(AbstractValue):
      schema = {}
  
      def __init__(self, **kwargs):
          for field in self.get_schema():
              setattr(self, field, kwargs.get(field))
  
      def __bool__(self):
          return any(
              getattr(self, field) for field in self.get_schema()
          )
  
      @classmethod
      def get_schema(cls):
          return cls.schema
  
      @classmethod
      def from_data(cls, data):
          if data is None:
              data = {}
          if not hasattr(data, 'get'):
              raise CantLoadData
          fields = {}
          for field, field_class in cls.get_schema().items():
              field_data = data.get(field)
              fields[field] = field_class.from_data(field_data)
          return cls(**fields)
  
      def as_data(self):
          data = {}
          for field, type in self.get_schema().items():
              value = as_data(getattr(self, field))
              if type.allow_as_data(value):
                  data[field] = value
          return data
  
  
  class ResourceIdentifier(Record):
      schema = {
          'type': Scalar,
          'id': Scalar,
      }
  
      # noinspection PyMissingConstructor
      def __init__(self, *, type=None, id=None):
          self.type = type
          self.id = id
  
  
  class SchemaAlternativeWrapper:
      def __init__(self, *types):
          self.types = types
  
      def from_data(self, data):
          return self.get_type(data).from_data(data)
  
      def allow_as_data(self, data):
          return self.get_type(data).allow_as_data(data)
  
      def get_type(self, data):
          exception = None
          for type in self.types:
              try:
                  type.from_data(data)
              except CantLoadData as e:
                  exception = e
              else:
                  return type
          if exception:
              raise exception
  
  
  class Relationship(Record):
      schema = {
          'data': SchemaAlternativeWrapper(
              ResourceIdentifier, make_collection(List, ResourceIdentifier, allow_empty_data=True)),
          'links': Dictionary,
      }
  
      # noinspection PyMissingConstructor
      def __init__(self, *, data=None, links=None):
          if data is None:
              self.data = ResourceIdentifier()
          else:
              self.data = data
          self.links = links or Dictionary()
  
  
  class JsonApiObject(Record):
      schema = {
          'type': Scalar,
          'id': Scalar,
          'attributes': Dictionary,
          'relationships': make_collection(Dictionary, Relationship),
          'links': Dictionary,
      }
  
      # noinspection PyMissingConstructor
      def __init__(self, *, type=None, id=None, attributes=None, relationships=None, links=None):
          self.type = type
          self.id = id
          self.attributes = attributes or Dictionary()
          self.relationships = relationships or Dictionary()
          self.links = links or Dictionary()
  
  
  class JsonApiResponse(Record):
      schema = {
          'data': SchemaAlternativeWrapper(JsonApiObject, make_collection(List, JsonApiObject, allow_empty_data=True)),
          'errors': List,
          'meta': Scalar,
          'jsonapi': Scalar,
          'links': Dictionary,
          'included': make_collection(List, JsonApiObject),
      }
  
      # noinspection PyMissingConstructor
      def __init__(self, *, data=None, errors=None, meta=None, jsonapi=None, links=None, included=None):
          if data is None:
              self.data = JsonApiObject()
          else:
              self.data = data
          self.errors = errors or List()
          self.meta = meta
          self.jsonapi = jsonapi
          self.links = links or Dictionary()
          self.included = included or List()
  
  
  class CantLoadData(Exception):
      pass
  