diff --git a/PKG-INFO b/PKG-INFO
index 5eff1167ae860b2b06b3a72adab41ae9e12ff384..3d39c374d56b1d83a49b803065f0036f34a2f859 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,14 +1,14 @@
 Metadata-Version: 2.1
 Name: swh.model
-Version: 0.0.32
+Version: 0.0.33
 Summary: Software Heritage data model
 Home-page: https://forge.softwareheritage.org/diffusion/DMOD/
 Author: Software Heritage developers
 Author-email: swh-devel@inria.fr
 License: UNKNOWN
+Project-URL: Bug Reports, https://forge.softwareheritage.org/maniphest
 Project-URL: Funding, https://www.softwareheritage.org/donate
 Project-URL: Source, https://forge.softwareheritage.org/source/swh-model
-Project-URL: Bug Reports, https://forge.softwareheritage.org/maniphest
 Description: swh-model
         =========
         
diff --git a/swh.model.egg-info/PKG-INFO b/swh.model.egg-info/PKG-INFO
index 5eff1167ae860b2b06b3a72adab41ae9e12ff384..3d39c374d56b1d83a49b803065f0036f34a2f859 100644
--- a/swh.model.egg-info/PKG-INFO
+++ b/swh.model.egg-info/PKG-INFO
@@ -1,14 +1,14 @@
 Metadata-Version: 2.1
 Name: swh.model
-Version: 0.0.32
+Version: 0.0.33
 Summary: Software Heritage data model
 Home-page: https://forge.softwareheritage.org/diffusion/DMOD/
 Author: Software Heritage developers
 Author-email: swh-devel@inria.fr
 License: UNKNOWN
+Project-URL: Bug Reports, https://forge.softwareheritage.org/maniphest
 Project-URL: Funding, https://www.softwareheritage.org/donate
 Project-URL: Source, https://forge.softwareheritage.org/source/swh-model
-Project-URL: Bug Reports, https://forge.softwareheritage.org/maniphest
 Description: swh-model
         =========
         
diff --git a/swh/model/hypothesis_strategies.py b/swh/model/hypothesis_strategies.py
index e2ca4c60bcdb6cab2c5b3c48683631289e52f465..26d4a817cdb7f834cd3cfc12c7238296cf2baa93 100644
--- a/swh/model/hypothesis_strategies.py
+++ b/swh/model/hypothesis_strategies.py
@@ -3,9 +3,11 @@
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+import datetime
+
 from hypothesis.strategies import (
-    lists, one_of, composite, builds, integers, sampled_from, binary,
-    dictionaries, none, from_regex, just
+    binary, builds, characters, composite, dictionaries, from_regex,
+    integers, just, lists, none, one_of, sampled_from, text, tuples,
 )
 
 
@@ -22,6 +24,10 @@ def sha1_git():
     return binary(min_size=20, max_size=20)
 
 
+def sha1():
+    return binary(min_size=20, max_size=20)
+
+
 @composite
 def urls(draw):
     protocol = draw(sampled_from(['git', 'http', 'https', 'deb']))
@@ -35,9 +41,11 @@ def persons():
 
 
 def timestamps():
+    max_seconds = datetime.datetime.max.timestamp()
+    min_seconds = datetime.datetime.min.timestamp()
     return builds(
         Timestamp,
-        seconds=integers(-2**63, 2**63-1),
+        seconds=integers(min_seconds, max_seconds),
         microseconds=integers(0, 1000000))
 
 
@@ -45,7 +53,7 @@ def timestamps_with_timezone():
     return builds(
         TimestampWithTimezone,
         timestamp=timestamps(),
-        offset=integers(-2**16, 2**16-1))
+        offset=integers(min_value=-14*60, max_value=14*60))
 
 
 def origins():
@@ -62,13 +70,27 @@ def origin_visits():
         origin=origins())
 
 
-def releases():
-    return builds(
+@composite
+def releases(draw):
+    (date, author) = draw(one_of(
+        tuples(none(), none()),
+        tuples(timestamps_with_timezone(), persons())))
+    rel = draw(builds(
         Release,
         id=sha1_git(),
-        date=timestamps_with_timezone(),
-        author=one_of(none(), persons()),
-        target=one_of(none(), sha1_git()))
+        author=none(),
+        date=none(),
+        target=sha1_git()))
+    rel.date = date
+    rel.author = author
+    return rel
+
+
+def revision_metadata():
+    alphabet = characters(
+        blacklist_categories=('Cs', ),
+        blacklist_characters=['\u0000'])  # postgresql does not like these
+    return dictionaries(text(alphabet=alphabet), text(alphabet=alphabet))
 
 
 def revisions():
@@ -77,9 +99,10 @@ def revisions():
         id=sha1_git(),
         date=timestamps_with_timezone(),
         committer_date=timestamps_with_timezone(),
-        parents=lists(binary()),
-        directory=binary(),
-        metadata=one_of(none(), dictionaries(binary(), binary())))
+        parents=lists(sha1_git()),
+        directory=sha1_git(),
+        metadata=one_of(none(), revision_metadata()))
+    # TODO: metadata['extra_headers'] can have binary keys and values
 
 
 def directory_entries():
@@ -96,18 +119,25 @@ def directories():
         entries=lists(directory_entries()))
 
 
-def contents():
-    def filter_data(content):
-        if content.status != 'visible':
-            content.data = None
-        return content
+@composite
+def contents(draw):
+    (status, data, reason) = draw(one_of(
+        tuples(just('visible'), binary(), none()),
+        tuples(just('absent'), none(), text()),
+        tuples(just('hidden'), none(), none()),
+    ))
 
-    return builds(
+    return draw(builds(
         Content,
         length=integers(0),
-        data=binary(),
+        sha1=sha1(),
         sha1_git=sha1_git(),
-    ).map(filter_data)
+        sha256=binary(min_size=32, max_size=32),
+        blake2s256=binary(min_size=32, max_size=32),
+        status=just(status),
+        data=just(data),
+        reason=just(reason),
+    ))
 
 
 def branch_names():
@@ -165,7 +195,6 @@ def snapshots(draw, *, min_size=0, max_size=100, only_objects=False):
                     name: branch.to_dict()
                     for (name, branch) in branches.items()}})
         except ValueError as e:
-            print(e.args)
             for (source, target) in e.args[1]:
                 branches[source] = draw(branch_targets(only_objects=True))
         else:
diff --git a/swh/model/identifiers.py b/swh/model/identifiers.py
index 486639e63560236c2e2b842623dd6e67f1fd0dc1..e7508ebcbbb4fbe05d1a5610725a2412750498ea 100644
--- a/swh/model/identifiers.py
+++ b/swh/model/identifiers.py
@@ -581,8 +581,6 @@ def snapshot_identifier(snapshot, *, ignore_unresolved=False):
             if target_id not in snapshot['branches'] or target_id == name:
                 unresolved.append((name, target_id))
         else:
-            print(name)
-            print(target)
             target_type = target['target_type'].encode()
             target_id = identifier_to_bytes(target['target'])
 
diff --git a/swh/model/model.py b/swh/model/model.py
index 890d1330a4eccc77110c9a115f8137e71e79e83a..036879de1425d49a21208f62be283b4f6bb7d92e 100644
--- a/swh/model/model.py
+++ b/swh/model/model.py
@@ -48,6 +48,13 @@ class TimestampWithTimezone:
     def to_dict(self):
         return attr.asdict(self)
 
+    @offset.validator
+    def check_offset(self, attribute, value):
+        if not (-2**15 <= value < 2**15):
+            # max 14 hours offset in theory, but you never know what
+            # you'll find in the wild...
+            raise ValueError('offset too large: %d minutes' % value)
+
 
 @attr.s
 class Origin:
@@ -83,6 +90,14 @@ class TargetType(Enum):
     ALIAS = 'alias'
 
 
+class ObjectType(Enum):
+    CONTENT = 'content'
+    DIRECTORY = 'directory'
+    REVISION = 'revision'
+    RELEASE = 'release'
+    SNAPSHOT = 'snapshot'
+
+
 @attr.s
 class SnapshotBranch:
     target = attr.ib(type=bytes)
@@ -121,18 +136,31 @@ class Release:
     id = attr.ib(type=Sha1Git)
     name = attr.ib(type=bytes)
     message = attr.ib(type=bytes)
-    date = attr.ib(type=TimestampWithTimezone)
+    date = attr.ib(type=Optional[TimestampWithTimezone])
     author = attr.ib(type=Optional[Person])
     target = attr.ib(type=Optional[Sha1Git])
-    target_type = attr.ib(type=TargetType)
+    target_type = attr.ib(type=ObjectType)
     synthetic = attr.ib(type=bool)
 
     def to_dict(self):
         rel = attr.asdict(self)
-        rel['date'] = self.date.to_dict()
+        rel['date'] = self.date.to_dict() if self.date is not None else None
         rel['target_type'] = rel['target_type'].value
         return rel
 
+    @author.validator
+    def check_author(self, attribute, value):
+        if self.author is None and self.date is not None:
+            raise ValueError('release date must be None if date is None.')
+
+
+class RevisionType(Enum):
+    GIT = 'git'
+    TAR = 'tar'
+    DSC = 'dsc'
+    SUBVERSION = 'svn'
+    MERCURIAL = 'hg'
+
 
 @attr.s
 class Revision:
@@ -143,15 +171,16 @@ class Revision:
     date = attr.ib(type=TimestampWithTimezone)
     committer_date = attr.ib(type=TimestampWithTimezone)
     parents = attr.ib(type=List[Sha1Git])
-    type = attr.ib(type=str)
+    type = attr.ib(type=RevisionType)
     directory = attr.ib(type=Sha1Git)
-    metadata = attr.ib(type=Optional[dict])
+    metadata = attr.ib(type=Optional[Dict[str, object]])
     synthetic = attr.ib(type=bool)
 
     def to_dict(self):
         rev = attr.asdict(self)
         rev['date'] = self.date.to_dict()
         rev['committer_date'] = self.committer_date.to_dict()
+        rev['type'] = rev['type'].value
         return rev
 
 
@@ -191,6 +220,7 @@ class Content:
     status = attr.ib(
         type=str,
         validator=attr.validators.in_(['visible', 'absent', 'hidden']))
+    reason = attr.ib(type=Optional[str])
 
     @length.validator
     def check_length(self, attribute, value):
@@ -198,8 +228,20 @@ class Content:
         if value < 0:
             raise ValueError('Length must be positive.')
 
+    @reason.validator
+    def check_reason(self, attribute, value):
+        """Checks the reason is full iff status != absent."""
+        assert self.reason == value
+        if self.status == 'absent' and value is None:
+            raise ValueError('Must provide a reason if content is absent.')
+        elif self.status != 'absent' and value is not None:
+            raise ValueError(
+                'Must not provide a reason if content is not absent.')
+
     def to_dict(self):
         content = attr.asdict(self)
         if content['data'] is None:
             del content['data']
+        if content['reason'] is None:
+            del content['reason']
         return content
diff --git a/swh/model/tests/test_hypothesis_strategies.py b/swh/model/tests/test_hypothesis_strategies.py
index 52e1211abcd2124efe7b39265e218b92e9cdc6ec..3e69ab9b93c8963c84f45023a0f89d9d61b3ad62 100644
--- a/swh/model/tests/test_hypothesis_strategies.py
+++ b/swh/model/tests/test_hypothesis_strategies.py
@@ -20,17 +20,38 @@ def test_generation(obj_type_and_obj):
     attr.validate(object_)
 
 
+def assert_nested_dict(obj):
+    """Tests the object is a nested dict and contains no more class
+    from swh.model.model."""
+    if isinstance(obj, dict):
+        for (key, value) in obj.items():
+            assert isinstance(key, (str, bytes)), key
+            assert_nested_dict(value)
+    elif isinstance(obj, list):
+        for value in obj:
+            assert_nested_dict(value)
+    elif isinstance(obj, (int, float, str, bytes, bool, type(None))):
+        pass
+    else:
+        assert False, obj
+
+
 @given(object_dicts())
 def test_dicts_generation(obj_type_and_obj):
     (obj_type, object_) = obj_type_and_obj
-    assert isinstance(object_, dict)
+    assert_nested_dict(object_)
     if obj_type == 'content':
         if object_['status'] == 'visible':
             assert set(object_) == \
                 set(DEFAULT_ALGORITHMS) | {'length', 'status', 'data'}
-        else:
+        elif object_['status'] == 'absent':
+            assert set(object_) == \
+                set(DEFAULT_ALGORITHMS) | {'length', 'status', 'reason'}
+        elif object_['status'] == 'hidden':
             assert set(object_) == \
                 set(DEFAULT_ALGORITHMS) | {'length', 'status'}
+        else:
+            assert False, object_
     elif obj_type == 'release':
         assert object_['target_type'] in target_types
     elif obj_type == 'snapshot':
diff --git a/version.txt b/version.txt
index 808b7d85a5dda3aba60ded8962aad41515e9e0d0..4d88e60adb09fdf158aaa9c6a58117136fe16bf0 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-v0.0.32-0-gd1b2156
\ No newline at end of file
+v0.0.33-0-gf9641d2
\ No newline at end of file