diff --git a/swh/model/model.py b/swh/model/model.py
index 2030da8c1b615d2330472254cd4cb6a4d53e079a..1781a3660b9c916cb742d34579b329e3593052b8 100644
--- a/swh/model/model.py
+++ b/swh/model/model.py
@@ -885,6 +885,17 @@ class MetadataFetcher(BaseModel):
         return {"name": self.name, "version": self.version}
 
 
+def normalize_discovery_date(value: Any) -> datetime.datetime:
+    if not isinstance(value, datetime.datetime):
+        raise TypeError("discovery_date must be a timezone-aware datetime.")
+
+    if value.tzinfo is None:
+        raise ValueError("discovery_date must be a timezone-aware datetime.")
+
+    # Normalize timezone to utc, and truncate microseconds to 0
+    return value.astimezone(datetime.timezone.utc).replace(microsecond=0)
+
+
 @attr.s(frozen=True, slots=True)
 class RawExtrinsicMetadata(HashableObject, BaseModel):
     object_type: Final = "raw_extrinsic_metadata"
@@ -893,7 +904,7 @@ class RawExtrinsicMetadata(HashableObject, BaseModel):
     target = attr.ib(type=ExtendedSWHID, validator=type_validator())
 
     # source
-    discovery_date = attr.ib(type=datetime.datetime, validator=type_validator())
+    discovery_date = attr.ib(type=datetime.datetime, converter=normalize_discovery_date)
     authority = attr.ib(type=MetadataAuthority, validator=type_validator())
     fetcher = attr.ib(type=MetadataFetcher, validator=type_validator())
 
@@ -923,12 +934,6 @@ class RawExtrinsicMetadata(HashableObject, BaseModel):
     def compute_hash(self) -> bytes:
         return hash_to_bytes(raw_extrinsic_metadata_identifier(self.to_dict()))
 
-    @discovery_date.validator
-    def check_discovery_date(self, attribute, value):
-        """Checks the discovery_date has a timezone."""
-        if value is not None and value.tzinfo is None:
-            raise ValueError("discovery_date must be a timezone-aware datetime.")
-
     @origin.validator
     def check_origin(self, attribute, value):
         if value is None:
diff --git a/swh/model/tests/test_model.py b/swh/model/tests/test_model.py
index 3c55b07cff38f6e750a2390fcff223e605bd2c18..5ea401c1befbe933b1d982dfebb80979527d9b9f 100644
--- a/swh/model/tests/test_model.py
+++ b/swh/model/tests/test_model.py
@@ -1140,3 +1140,39 @@ def test_metadata_validate_context_directory():
             ),
             **_common_metadata_fields,
         )
+
+
+def test_metadata_normalize_discovery_date():
+    fields_copy = {**_common_metadata_fields}
+    truncated_date = fields_copy.pop("discovery_date")
+    assert truncated_date.microsecond == 0
+
+    # Check for TypeError on disabled object type: we removed attrs_strict's
+    # type_validator
+    with pytest.raises(TypeError):
+        RawExtrinsicMetadata(
+            target=_content_swhid, discovery_date="not a datetime", **fields_copy
+        )
+
+    # Check for truncation to integral second
+    date_with_us = truncated_date.replace(microsecond=42)
+    md = RawExtrinsicMetadata(
+        target=_content_swhid, discovery_date=date_with_us, **fields_copy,
+    )
+
+    assert md.discovery_date == truncated_date
+    assert md.discovery_date.tzinfo == datetime.timezone.utc
+
+    # Check that the timezone gets normalized. Timezones can be offset by a
+    # non-integral number of seconds, so we need to handle that.
+    timezone = datetime.timezone(offset=datetime.timedelta(hours=2))
+    date_with_tz = truncated_date.astimezone(timezone)
+
+    assert date_with_tz.tzinfo != datetime.timezone.utc
+
+    md = RawExtrinsicMetadata(
+        target=_content_swhid, discovery_date=date_with_tz, **fields_copy,
+    )
+
+    assert md.discovery_date == truncated_date
+    assert md.discovery_date.tzinfo == datetime.timezone.utc