Index: builder/exam_elements.py
===================================================================
--- builder/exam_elements.py	(revision 066f7d9db9dbc52af253b10b3e70a957f2e40817)
+++ builder/exam_elements.py	(revision aaaa4b88ba1d686bf0ed49967c79cccb30b41f94)
@@ -5,5 +5,5 @@
 from dataclasses import dataclass, field
 from flexoentity import FlexoEntity, EntityType, EntityState, FlexOID, logger
-from .media_items import MediaItem, NullMediaItem, DownloadItem
+from builder.media_items import MediaItem, NullMediaItem, DownloadItem, media_factory
 
 
@@ -47,9 +47,9 @@
     text: str = ""
     topic: str = ""
-    media: List[MediaItem] = field(default_factory=lambda: [NullMediaItem()])
+    media: List[MediaItem] = field(default_factory=lambda: [])
 
     @classmethod
     def default(cls):
-        return cls()
+        return cls.with_domain_id(domain_id="DEF_DEFAULT")
 
     @property
@@ -98,4 +98,28 @@
     def media_to_html(self, prefix="media/") -> str:
         return "".join([m.to_html(prefix=prefix) for m in self.media if m])
+
+    @classmethod
+    def from_dict(cls, data):
+        if "flexo_id" not in data:
+            raise ValueError("Question is missing flexo_id")
+
+        flexo_id = FlexOID(data["flexo_id"])
+
+        # NOTE: cls(...) will call the correct subclass constructor
+        obj = cls(
+            text=data.get("text", ""),
+            topic=data.get("topic", ""),
+            flexo_id=flexo_id,
+        )
+
+        # restore provenance
+        obj.fingerprint = data.get("fingerprint", "")
+        obj.origin = data.get("origin")
+        if data.get("originator_id"):
+            obj.originator_id = UUID(data["originator_id"])
+        if data.get("owner_id"):
+            obj.owner_id = UUID(data["owner_id"])
+
+        return obj
 
     def to_dict(self):
@@ -105,5 +129,5 @@
             "qtype": self.qtype,  # avoid name clash with built-in type
             "text": self.text,
-            "media": [m.to_dict() for m in self.media if not isinstance(m, NullMediaItem)],
+            "media": [m.to_dict() for m in self.media],
             "state": self.state.name,  # explicit readable form
         })
@@ -111,8 +135,8 @@
 
     def has_media(self):
-        return any(not isinstance(item, NullMediaItem) for item in self.media)
+        return any(item for item in self.media)
 
     def get_media(self):
-        return [item for item in self.media if not isinstance(item, NullMediaItem)]
+        return [item for item in self.media]
 
 
@@ -154,4 +178,10 @@
         return d
 
+    @classmethod
+    def from_dict(cls, data):
+        obj = super().from_dict(data)
+        obj.media = [media_factory(m) for m in data.get("media", [])]
+        return obj
+
 
 @dataclass
@@ -187,4 +217,15 @@
         })
         return base
+
+    @classmethod
+    def from_dict(cls, data):
+        obj = super().from_dict(data)
+        obj.options = [
+            AnswerOption(o["id"], o["text"], o.get("points", 0.0))
+            for o in data.get("options", [])
+        ]
+
+        obj.media = [media_factory(m) for m in data.get("media", [])]
+        return obj
 
     def points_for(self, option_id):
@@ -280,10 +321,9 @@
 
     @classmethod
-    def from_dict(cls, data: dict) -> "TextQuestion":
-        validation = None
-        if "validation" in data and isinstance(data["validation"], dict):
-            validation = Validation(**data["validation"])
+    def from_dict(cls, data):
         obj = super().from_dict(data)
-        obj.validation = validation
+        v = data.get("validation")
+        obj.validation = Validation(**v) if v else None
+        obj.media = [media_factory(m) for m in data.get("media", [])]
         return obj
 
@@ -363,17 +403,10 @@
         return d
 
-    @staticmethod
-    def from_dict(data: dict) -> "CandidateIDQuestion":
-        q = CandidateIDQuestion(text=data["text"],
-                                topic=data.get("topic", ""),
-                                fields=data.get("fields", []),
-        )
-        flexo_id = data.get("id",
-                       FlexOID.safe_generate(domain="SYS",
-                                             entity_type=EntityType.ITEM,
-                                             entity_state=EntityState.DRAFT,
-                                             ))
-        q.flexo_id = flexo_id
-        return q
+    @classmethod
+    def from_dict(cls, data):
+        obj = super().from_dict(data)
+        obj.fields = data.get("fields", [])
+        obj.media = [media_factory(m) for m in data.get("media", [])]
+        return obj
 
 @dataclass
