diff --git a/PKG-INFO b/PKG-INFO
index 468d926da6dfe3a712738ce507759f2f978dfec9..cd3b55ba1a9c82d643e23a13f02c19d5a3db0d66 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,14 +1,14 @@
 Metadata-Version: 2.1
 Name: swh.model
-Version: 0.0.36
+Version: 0.0.37
 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: Source, https://forge.softwareheritage.org/source/swh-model
-Project-URL: Bug Reports, https://forge.softwareheritage.org/maniphest
 Project-URL: Funding, https://www.softwareheritage.org/donate
+Project-URL: Bug Reports, https://forge.softwareheritage.org/maniphest
+Project-URL: Source, https://forge.softwareheritage.org/source/swh-model
 Description: swh-model
         =========
         
diff --git a/requirements-swh.txt b/requirements-swh.txt
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..32aa75ec6ce17c3177f18150967201f13ef774bb 100644
--- a/requirements-swh.txt
+++ b/requirements-swh.txt
@@ -0,0 +1 @@
+swh.core >= 0.0.60
diff --git a/setup.py b/setup.py
index c28e4bf0ad4ef5a6931ae5e986ab0b66b64569b0..0e24d22bb117992517682eaf87c785644cabae8c 100755
--- a/setup.py
+++ b/setup.py
@@ -73,6 +73,8 @@ setup(
     entry_points='''
         [console_scripts]
         swh-identify=swh.model.cli:identify
+        [swh.cli.subcommands]
+        identify=swh.model.cli:identify
     ''',
     classifiers=[
         "Programming Language :: Python :: 3",
diff --git a/swh.model.egg-info/PKG-INFO b/swh.model.egg-info/PKG-INFO
index 468d926da6dfe3a712738ce507759f2f978dfec9..cd3b55ba1a9c82d643e23a13f02c19d5a3db0d66 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.36
+Version: 0.0.37
 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: Source, https://forge.softwareheritage.org/source/swh-model
-Project-URL: Bug Reports, https://forge.softwareheritage.org/maniphest
 Project-URL: Funding, https://www.softwareheritage.org/donate
+Project-URL: Bug Reports, https://forge.softwareheritage.org/maniphest
+Project-URL: Source, https://forge.softwareheritage.org/source/swh-model
 Description: swh-model
         =========
         
diff --git a/swh.model.egg-info/entry_points.txt b/swh.model.egg-info/entry_points.txt
index 9c74f3660b342d539bac70c1973a1cea205cbb7a..03eb1114b8c543f6bcdd3518f05ecbd259b5c3e9 100644
--- a/swh.model.egg-info/entry_points.txt
+++ b/swh.model.egg-info/entry_points.txt
@@ -1,4 +1,6 @@
 
         [console_scripts]
         swh-identify=swh.model.cli:identify
+        [swh.cli.subcommands]
+        identify=swh.model.cli:identify
     
\ No newline at end of file
diff --git a/swh.model.egg-info/requires.txt b/swh.model.egg-info/requires.txt
index af8f5be4c20b71a50eb1d2e0478234978170c263..d8298156bb5103e8de007f314803501c9caaeae4 100644
--- a/swh.model.egg-info/requires.txt
+++ b/swh.model.egg-info/requires.txt
@@ -3,6 +3,7 @@ Click
 attrs
 hypothesis
 python-dateutil
+swh.core>=0.0.60
 
 [testing]
 pytest
diff --git a/swh/model/cli.py b/swh/model/cli.py
index 82af76f8cd5827167133edc1bb0556145d5d688e..de04bb17696b26bb0ec39460f9b1de7ba9098049 100644
--- a/swh/model/cli.py
+++ b/swh/model/cli.py
@@ -9,6 +9,8 @@ import sys
 
 from functools import partial
 
+from swh.core.cli import CONTEXT_SETTINGS
+
 from swh.model import identifiers as pids
 from swh.model.exceptions import ValidationError
 from swh.model.from_disk import Content, Directory
@@ -62,7 +64,7 @@ def identify_object(obj_type, follow_symlinks, obj):
     return (obj, pid)
 
 
-@click.command()
+@click.command(context_settings=CONTEXT_SETTINGS)
 @click.option('--dereference/--no-dereference', 'follow_symlinks',
               default=True,
               help='follow (or not) symlinks for OBJECTS passed as arguments '
@@ -74,7 +76,7 @@ def identify_object(obj_type, follow_symlinks, obj):
               help='type of object to identify (default: auto)')
 @click.option('--verify', '-v', metavar='PID', type=PidParamType(),
               help='reference identifier to be compared with computed one')
-@click.argument('objects', nargs=-1,
+@click.argument('objects', nargs=-1, required=True,
                 type=click.Path(exists=True, readable=True,
                                 allow_dash=True, path_type=bytes))
 def identify(obj_type, verify, show_filename, follow_symlinks, objects):
@@ -90,13 +92,13 @@ def identify(obj_type, verify, show_filename, follow_symlinks, objects):
     Examples:
 
     \b
-      $ swh-identify fork.c kmod.c sched/deadline.c
+      $ swh identify fork.c kmod.c sched/deadline.c
       swh:1:cnt:2e391c754ae730bd2d8520c2ab497c403220c6e3    fork.c
       swh:1:cnt:0277d1216f80ae1adeed84a686ed34c9b2931fc2    kmod.c
       swh:1:cnt:57b939c81bce5d06fa587df8915f05affbe22b82    sched/deadline.c
 
     \b
-      $ swh-identify --no-filename /usr/src/linux/kernel/
+      $ swh identify --no-filename /usr/src/linux/kernel/
       swh:1:dir:f9f858a48d663b3809c9e2f336412717496202ab
 
     """
diff --git a/swh/model/hypothesis_strategies.py b/swh/model/hypothesis_strategies.py
index 3e006c8afaae1d5ec8f51c6135ae7023fbabc062..1b99957a737030dab80f3f01f204a925e6d2621f 100644
--- a/swh/model/hypothesis_strategies.py
+++ b/swh/model/hypothesis_strategies.py
@@ -20,6 +20,15 @@ from .model import (
 from .identifiers import snapshot_identifier, identifier_to_bytes
 
 
+pgsql_alphabet = characters(
+    blacklist_categories=('Cs', ),
+    blacklist_characters=['\u0000'])  # postgresql does not like these
+
+
+def pgsql_text():
+    return text(alphabet=pgsql_alphabet)
+
+
 def sha1_git():
     return binary(min_size=20, max_size=20)
 
@@ -89,10 +98,7 @@ def releases(draw):
 
 
 def revision_metadata():
-    alphabet = characters(
-        blacklist_categories=('Cs', ),
-        blacklist_characters=['\u0000'])  # postgresql does not like these
-    return dictionaries(text(alphabet=alphabet), text(alphabet=alphabet))
+    return dictionaries(pgsql_text(), pgsql_text())
 
 
 def revisions():
@@ -125,7 +131,7 @@ def directories():
 def contents(draw):
     (status, data, reason) = draw(one_of(
         tuples(just('visible'), binary(), none()),
-        tuples(just('absent'), none(), text()),
+        tuples(just('absent'), none(), pgsql_text()),
         tuples(just('hidden'), none(), none()),
     ))
 
@@ -143,7 +149,7 @@ def contents(draw):
 
 
 def branch_names():
-    return binary()
+    return binary(min_size=1)
 
 
 def branch_targets_object():
diff --git a/swh/model/model.py b/swh/model/model.py
index 25a565b4c5560f7fb894887064995b1f4bd923d2..6e3fd0e0a7d3f14bede8569e54629022a2df817c 100644
--- a/swh/model/model.py
+++ b/swh/model/model.py
@@ -16,20 +16,6 @@ from .identifiers import normalize_timestamp
 Sha1Git = bytes
 
 
-def contains_optional_validator(validator):
-    """Inspects an attribute's validator to find its type.
-    Inspired by `hypothesis/searchstrategy/attrs.py`."""
-    if isinstance(validator, attr.validators._OptionalValidator):
-        return True
-    elif isinstance(validator, attr.validators._AndValidator):
-        for validator in validator._validators:
-            res = contains_optional_validator(validator)
-            if res:
-                return True
-    else:
-        return False
-
-
 class BaseModel:
     """Base class for SWH model classes.
 
@@ -45,31 +31,7 @@ class BaseModel:
     def from_dict(cls, d):
         """Takes a dictionary representing a tree of SWH objects, and
         recursively builds the corresponding objects."""
-        if not isinstance(d, dict):
-            raise TypeError(
-                '%s.from_dict expects a dict, not %r' % (cls.__name__, d))
-        kwargs = {}
-        for (name, attribute) in attr.fields_dict(cls).items():
-            type_ = attribute.type
-
-            # Heuristic to detect `Optional[X]` and unwrap it to `X`.
-            if contains_optional_validator(attribute.validator):
-                if name not in d:
-                    continue
-                if d[name] is None:
-                    continue
-                else:
-                    type_ = type_.__args__[0]
-
-            # Construct an object of the expected type
-            if issubclass(type_, BaseModel):
-                kwargs[name] = type_.from_dict(d[name])
-            elif issubclass(type_, Enum):
-                kwargs[name] = type_(d[name])
-            else:
-                kwargs[name] = d[name]
-
-        return cls(**kwargs)
+        return cls(**d)
 
 
 @attr.s
@@ -119,7 +81,11 @@ class TimestampWithTimezone(BaseModel):
     def from_dict(cls, d):
         """Builds a TimestampWithTimezone from any of the formats
         accepted by :py:`swh.model.normalize_timestamp`."""
-        return super().from_dict(normalize_timestamp(d))
+        d = normalize_timestamp(d)
+        return cls(
+            timestamp=Timestamp.from_dict(d['timestamp']),
+            offset=d['offset'],
+            negative_utc=d['negative_utc'])
 
 
 @attr.s
@@ -197,6 +163,12 @@ class SnapshotBranch(BaseModel):
         branch['target_type'] = branch['target_type'].value
         return branch
 
+    @classmethod
+    def from_dict(cls, d):
+        return cls(
+            target=d['target'],
+            target_type=TargetType(d['target_type']))
+
 
 @attr.s
 class Snapshot(BaseModel):
@@ -215,14 +187,12 @@ class Snapshot(BaseModel):
 
     @classmethod
     def from_dict(cls, d):
-        d = {
-            **d,
-            'branches': {
+        return cls(
+            id=d['id'],
+            branches={
                 name: SnapshotBranch.from_dict(branch)
                 for (name, branch) in d['branches'].items()
-            }
-        }
-        return cls(**d)
+            })
 
 
 @attr.s
@@ -253,6 +223,17 @@ class Release(BaseModel):
         rel['target_type'] = rel['target_type'].value
         return rel
 
+    @classmethod
+    def from_dict(cls, d):
+        d = d.copy()
+        if d.get('author'):
+            d['author'] = Person.from_dict(d['author'])
+        if d.get('date'):
+            d['date'] = TimestampWithTimezone.from_dict(d['date'])
+        return cls(
+            target_type=ObjectType(d.pop('target_type')),
+            **d)
+
 
 class RevisionType(Enum):
     GIT = 'git'
@@ -286,6 +267,22 @@ class Revision(BaseModel):
         rev['type'] = rev['type'].value
         return rev
 
+    @classmethod
+    def from_dict(cls, d):
+        return cls(
+            id=d['id'],
+            message=d['message'],
+            author=Person.from_dict(d['author']),
+            committer=Person.from_dict(d['committer']),
+            date=TimestampWithTimezone.from_dict(d['date']),
+            committer_date=TimestampWithTimezone.from_dict(
+                d['committer_date']),
+            type=RevisionType(d['type']),
+            directory=d['directory'],
+            synthetic=d['synthetic'],
+            metadata=d['metadata'],
+            parents=d['parents'])
+
 
 @attr.s
 class DirectoryEntry(BaseModel):
@@ -309,11 +306,10 @@ class Directory(BaseModel):
 
     @classmethod
     def from_dict(cls, d):
-        d = {
-            **d,
-            'entries': list(map(DirectoryEntry.from_dict, d['entries']))
-        }
-        return super().from_dict(d)
+        return cls(
+            id=d['id'],
+            entries=[DirectoryEntry.from_dict(entry)
+                     for entry in d['entries']])
 
 
 @attr.s
diff --git a/version.txt b/version.txt
index bcfdf8a972c71985a51394b567f98b129ec6fa28..6f92dd26a77e0fb4056cdafaf732853b6f097457 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-v0.0.36-0-gfc3d3c1
\ No newline at end of file
+v0.0.37-0-gd7ec4a6
\ No newline at end of file