diff --git a/swh/model/model.py b/swh/model/model.py
index 6824217d4977eef188e0a81cd6e8ed6610ce6498..217e3632b6d2976a55b90f54f581ccf68bcb7776 100644
--- a/swh/model/model.py
+++ b/swh/model/model.py
@@ -28,7 +28,21 @@ class BaseModel:
     def to_dict(self):
         """Wrapper of `attr.asdict` that can be overridden by subclasses
         that have special handling of some of the fields."""
-        return attr.asdict(self)
+
+        def dictify(value):
+            if isinstance(value, BaseModel):
+                return value.to_dict()
+            elif isinstance(value, Enum):
+                return value.value
+            elif isinstance(value, dict):
+                return {k: dictify(v) for k, v in value.items()}
+            elif isinstance(value, list):
+                return [dictify(v) for v in value]
+            else:
+                return value
+
+        ret = attr.asdict(self, recurse=False)
+        return dictify(ret)
 
     @classmethod
     def from_dict(cls, d):
@@ -127,7 +141,6 @@ class OriginVisit(BaseModel):
         ov = super().to_dict()
         if ov['visit'] is None:
             del ov['visit']
-        ov['origin'] = self.origin.to_dict()
         return ov
 
     @classmethod
@@ -178,11 +191,6 @@ class SnapshotBranch(BaseModel):
                 raise ValueError('Wrong length for bytes identifier: %d' %
                                  len(value))
 
-    def to_dict(self):
-        branch = attr.asdict(self)
-        branch['target_type'] = branch['target_type'].value
-        return branch
-
     @classmethod
     def from_dict(cls, d):
         return cls(
@@ -196,15 +204,6 @@ class Snapshot(BaseModel):
     id = attr.ib(type=Sha1Git)
     branches = attr.ib(type=Dict[bytes, Optional[SnapshotBranch]])
 
-    def to_dict(self):
-        return {
-            'id': self.id,
-            'branches': {
-                name: branch.to_dict() if branch else None
-                for (name, branch) in self.branches.items()
-            }
-        }
-
     @classmethod
     def from_dict(cls, d):
         return cls(
@@ -237,9 +236,7 @@ class Release(BaseModel):
             raise ValueError('release date must be None if author is None.')
 
     def to_dict(self):
-        rel = attr.asdict(self)
-        rel['date'] = self.date.to_dict() if self.date is not None else None
-        rel['target_type'] = rel['target_type'].value
+        rel = super().to_dict()
         if rel['metadata'] is None:
             del rel['metadata']
         return rel
@@ -280,13 +277,6 @@ class Revision(BaseModel):
     parents = attr.ib(type=List[Sha1Git],
                       default=attr.Factory(list))
 
-    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
-
     @classmethod
     def from_dict(cls, d):
         d = d.copy()
@@ -316,11 +306,6 @@ class Directory(BaseModel):
     id = attr.ib(type=Sha1Git)
     entries = attr.ib(type=List[DirectoryEntry])
 
-    def to_dict(self):
-        dir_ = attr.asdict(self)
-        dir_['entries'] = [entry.to_dict() for entry in self.entries]
-        return dir_
-
     @classmethod
     def from_dict(cls, d):
         return cls(
@@ -367,7 +352,7 @@ class Content(BaseModel):
                 'Must not provide a reason if content is not absent.')
 
     def to_dict(self):
-        content = attr.asdict(self)
+        content = super().to_dict()
         for field in ('data', 'reason', 'ctime'):
             if content[field] is None:
                 del content[field]