diff --git a/swh/model/swhids.py b/swh/model/swhids.py
index ef04b96978c26a9cccebd86a420ab17260774cb8..d456b40cc007388c3d5e7fd794ebca1f4f0569e1 100644
--- a/swh/model/swhids.py
+++ b/swh/model/swhids.py
@@ -123,6 +123,9 @@ class _BaseSWHID(Generic[_TObjectType]):
             )
 
     def __str__(self) -> str:
+        return self._format_core_swhid()
+
+    def _format_core_swhid(self) -> str:
         return SWHID_SEP.join(
             [
                 self.namespace,
@@ -291,8 +294,9 @@ class QualifiedSWHID(_BaseSWHID[ObjectType]):
     when the anchor denotes a snapshot, the root directory is the one pointed to by HEAD
     (possibly indirectly), and undefined if such a reference is missing"""
 
+    Lines = Tuple[int, Optional[int]]
     lines = attr.ib(
-        type=Optional[Tuple[int, Optional[int]]],
+        type=Optional[Lines],
         default=None,
         validator=type_validator(),
         converter=_parse_lines_qualifier,
@@ -321,6 +325,17 @@ class QualifiedSWHID(_BaseSWHID[ObjectType]):
                 params={"type": value.object_type.value},
             )
 
+    def to_dict(self) -> Dict[str, Optional[str | bytes | CoreSWHID | Lines]]:
+        """Returns a dictionary version of this QSWHID for json serialization"""
+        return {
+            "swhid": self._format_core_swhid(),
+            "origin": self.origin,
+            "visit": self.visit,
+            "anchor": self.anchor,
+            "path": self.path,
+            "lines": self.lines,
+        }
+
     def qualifiers(self) -> Dict[str, str]:
         """Returns URL-escaped qualifiers of this SWHID, for use in serialization"""
         origin = self.origin
@@ -350,14 +365,7 @@ class QualifiedSWHID(_BaseSWHID[ObjectType]):
         return {k: v for (k, v) in d.items() if v is not None}
 
     def __str__(self) -> str:
-        swhid = SWHID_SEP.join(
-            [
-                self.namespace,
-                str(self.scheme_version),
-                self.object_type.value,
-                hash_to_hex(self.object_id),
-            ]
-        )
+        swhid = self._format_core_swhid()
         qualifiers = self.qualifiers()
         if qualifiers:
             for k, v in qualifiers.items():
diff --git a/swh/model/tests/test_swhids.py b/swh/model/tests/test_swhids.py
index 4236b8e18243b4625a76da0c4f6eea3a8231d5d2..09d503d5b8077108df3be77436a6d7aec4418deb 100644
--- a/swh/model/tests/test_swhids.py
+++ b/swh/model/tests/test_swhids.py
@@ -407,6 +407,24 @@ def test_QualifiedSWHID_init(object_type, qualifiers, expected):
         assert QualifiedSWHID.from_string(expected) == swhid
 
 
+@pytest.mark.parametrize(
+    "object_type,qualifiers",
+    [
+        (type_, dict_)
+        for (type_, dict_, str_or_exc) in QSWHID_EXPECTED
+        if isinstance(str_or_exc, str)
+    ],
+)
+def test_QualifiedSWHID_to_dict(object_type, qualifiers):
+    qswhid = QualifiedSWHID(object_type=object_type, object_id=_x(HASH), **qualifiers)
+    d = qswhid.to_dict()
+    swhid = CoreSWHID.from_string(d.pop("swhid"))
+    other = QualifiedSWHID(
+        object_type=swhid.object_type, object_id=swhid.object_id, **d
+    )
+    assert qswhid == other
+
+
 def test_QualifiedSWHID_hash():
     object_id = _x("94a9ed024d3859793618152ea559a168bbcbb5e2")