diff --git a/swh/model/hypothesis_strategies.py b/swh/model/hypothesis_strategies.py
index a647aa8d13bd73ef2e8e296443f27877601e8375..2dc601b1b26116f2f9879f4eea95afa511aebfd7 100644
--- a/swh/model/hypothesis_strategies.py
+++ b/swh/model/hypothesis_strategies.py
@@ -239,6 +239,7 @@ def present_contents_d():
     return builds(
         dict,
         data=binary(max_size=4096),
+        ctime=optional(datetimes()),
         status=one_of(just('visible'), just('hidden')),
     )
 
@@ -258,6 +259,7 @@ def skipped_contents_d(draw):
         result[k] = None
     result['reason'] = draw(pgsql_text())
     result['status'] = 'absent'
+    result['ctime'] = draw(optional(datetimes()))
     return result
 
 
diff --git a/swh/model/model.py b/swh/model/model.py
index 5cc40afe150d12ae17ea15213cae2e88fd6ca7b6..95fe19900c1fa3435358995fb5f62775b9e0e6b4 100644
--- a/swh/model/model.py
+++ b/swh/model/model.py
@@ -554,12 +554,6 @@ class BaseContent(BaseModel):
 
         return d
 
-    def to_dict(self):
-        content = super().to_dict()
-        if content['ctime'] is None:
-            del content['ctime']
-        return content
-
     @classmethod
     def from_dict(cls, d, use_subclass=True):
         if use_subclass:
@@ -628,7 +622,7 @@ class Content(BaseContent):
         return content
 
     @classmethod
-    def from_data(cls, data, status='visible') -> 'Content':
+    def from_data(cls, data, status='visible', ctime=None) -> 'Content':
         """Generate a Content from a given `data` byte string.
 
         This populates the Content with the hashes and length for the data
@@ -636,10 +630,14 @@ class Content(BaseContent):
         """
         d = cls._hash_data(data)
         d['status'] = status
+        d['ctime'] = ctime
         return cls(**d)
 
     @classmethod
     def from_dict(cls, d):
+        if isinstance(d.get('ctime'), str):
+            d = d.copy()
+            d['ctime'] = dateutil.parser.parse(d['ctime'])
         return super().from_dict(d, use_subclass=False)
 
     def with_data(self) -> 'Content':
@@ -710,7 +708,11 @@ class SkippedContent(BaseContent):
         return content
 
     @classmethod
-    def from_data(cls, data, reason: str) -> 'SkippedContent':
+    def from_data(
+            cls,
+            data: bytes,
+            reason: str,
+            ctime: Optional[datetime.datetime] = None) -> 'SkippedContent':
         """Generate a SkippedContent from a given `data` byte string.
 
         This populates the SkippedContent with the hashes and length for the
@@ -723,6 +725,7 @@ class SkippedContent(BaseContent):
         del d['data']
         d['status'] = 'absent'
         d['reason'] = reason
+        d['ctime'] = ctime
         return cls(**d)
 
     @classmethod
diff --git a/swh/model/tests/test_hypothesis_strategies.py b/swh/model/tests/test_hypothesis_strategies.py
index 59b4eabc307e91aa4164eae7850d5a049dbf4d77..dd77c8de4e6c742233ade4d8617704b04bb22d41 100644
--- a/swh/model/tests/test_hypothesis_strategies.py
+++ b/swh/model/tests/test_hypothesis_strategies.py
@@ -44,15 +44,13 @@ def test_dicts_generation(obj_type_and_obj):
     (obj_type, object_) = obj_type_and_obj
     assert_nested_dict(object_)
     if obj_type == 'content':
+        COMMON_KEYS = set(DEFAULT_ALGORITHMS) | {'length', 'status', 'ctime'}
         if object_['status'] == 'visible':
-            assert set(object_) <= \
-                set(DEFAULT_ALGORITHMS) | {'length', 'status', 'data'}
+            assert set(object_) <= COMMON_KEYS | {'data'}
         elif object_['status'] == 'absent':
-            assert set(object_) == \
-                set(DEFAULT_ALGORITHMS) | {'length', 'status', 'reason'}
+            assert set(object_) == COMMON_KEYS | {'reason'}
         elif object_['status'] == 'hidden':
-            assert set(object_) <= \
-                set(DEFAULT_ALGORITHMS) | {'length', 'status', 'data'}
+            assert set(object_) <= COMMON_KEYS | {'data'}
         else:
             assert False, object_
     elif obj_type == 'release':
@@ -68,15 +66,13 @@ def test_model_to_dicts(obj_type_and_obj):
     obj_dict = object_.to_dict()
     assert_nested_dict(obj_dict)
     if obj_type == 'content':
+        COMMON_KEYS = set(DEFAULT_ALGORITHMS) | {'length', 'status', 'ctime'}
         if obj_dict['status'] == 'visible':
-            assert set(obj_dict) == \
-                set(DEFAULT_ALGORITHMS) | {'length', 'status', 'data'}
+            assert set(obj_dict) == COMMON_KEYS | {'data'}
         elif obj_dict['status'] == 'absent':
-            assert set(obj_dict) == \
-                set(DEFAULT_ALGORITHMS) | {'length', 'status', 'reason'}
+            assert set(obj_dict) == COMMON_KEYS | {'reason'}
         elif obj_dict['status'] == 'hidden':
-            assert set(obj_dict) == \
-                set(DEFAULT_ALGORITHMS) | {'length', 'status', 'data'}
+            assert set(obj_dict) == COMMON_KEYS | {'data'}
         else:
             assert False, obj_dict
     elif obj_type == 'release':
diff --git a/swh/model/tests/test_model.py b/swh/model/tests/test_model.py
index 4bbb1d0741dbbe1eaba84908632c9fa229d67e4e..f6164ef1a64bfa4890e8593a12019d87a3f17ef5 100644
--- a/swh/model/tests/test_model.py
+++ b/swh/model/tests/test_model.py
@@ -18,11 +18,7 @@ from swh.model.model import (
     MissingData, Person
 )
 from swh.model.hashutil import hash_to_bytes, MultiHash
-
-from swh.model.hypothesis_strategies import (
-    objects, origins, origin_visits, origin_visit_updates,
-    skipped_contents_d, timestamps
-)
+import swh.model.hypothesis_strategies as strategies
 from swh.model.identifiers import (
     directory_identifier, revision_identifier, release_identifier,
     snapshot_identifier
@@ -32,7 +28,7 @@ from swh.model.tests.test_identifiers import (
 )
 
 
-@given(objects())
+@given(strategies.objects())
 def test_todict_inverse_fromdict(objtype_and_obj):
     (obj_type, obj) = objtype_and_obj
 
@@ -52,7 +48,7 @@ def test_todict_inverse_fromdict(objtype_and_obj):
     assert obj_as_dict == type(obj).from_dict(obj_as_dict).to_dict()
 
 
-@given(origins())
+@given(strategies.origins())
 def test_todict_origins(origin):
     obj = origin.to_dict()
 
@@ -60,14 +56,14 @@ def test_todict_origins(origin):
     assert type(origin)(url=origin.url) == type(origin).from_dict(obj)
 
 
-@given(origin_visits())
+@given(strategies.origin_visits())
 def test_todict_origin_visits(origin_visit):
     obj = origin_visit.to_dict()
 
     assert origin_visit == type(origin_visit).from_dict(obj)
 
 
-@given(origin_visit_updates())
+@given(strategies.origin_visit_updates())
 def test_todict_origin_visit_updates(origin_visit_update):
     obj = origin_visit_update.to_dict()
 
@@ -76,7 +72,7 @@ def test_todict_origin_visit_updates(origin_visit_update):
 
 # Timestamp
 
-@given(timestamps())
+@given(strategies.timestamps())
 def test_timestamps_strategy(timestamp):
     attr.validate(timestamp)
 
@@ -326,6 +322,8 @@ def test_git_author_line_to_author():
         assert expected_person == Person.from_fullname(person)
 
 
+# Content
+
 def test_content_get_hash():
     hashes = dict(
         sha1=b'foo', sha1_git=b'bar', sha256=b'baz', blake2s256=b'qux')
@@ -356,6 +354,33 @@ def test_content_data_missing():
         c.with_data()
 
 
+@given(strategies.present_contents_d())
+def test_content_from_dict(content_d):
+    c = Content.from_data(**content_d)
+    assert c
+    assert c.ctime == content_d['ctime']
+
+    content_d2 = c.to_dict()
+    c2 = Content.from_dict(content_d2)
+    assert c2.ctime == c.ctime
+
+
+def test_content_from_dict_str_ctime():
+    # test with ctime as a string
+    n = datetime.datetime(2020, 5, 6, 12, 34)
+    content_d = {
+        'ctime': n.isoformat(),
+        'data': b'',
+        'length': 0,
+        'sha1': b'\x00',
+        'sha256': b'\x00',
+        'sha1_git': b'\x00',
+        'blake2s256': b'\x00',
+        }
+    c = Content.from_dict(content_d)
+    assert c.ctime == n
+
+
 @given(binary(max_size=4096))
 def test_content_from_data(data):
     c = Content.from_data(data)
@@ -376,6 +401,8 @@ def test_hidden_content_from_data(data):
         assert getattr(c, key) == value
 
 
+# SkippedContent
+
 @given(binary(max_size=4096))
 def test_skipped_content_from_data(data):
     c = SkippedContent.from_data(data, reason='reason')
@@ -386,7 +413,7 @@ def test_skipped_content_from_data(data):
         assert getattr(c, key) == value
 
 
-@given(skipped_contents_d())
+@given(strategies.skipped_contents_d())
 def test_skipped_content_origin_is_str(skipped_content_d):
     assert SkippedContent.from_dict(skipped_content_d)