Repository: cogent3/cogent3
Pull request #2384 (2025-06-29 03:20:47)

ENH: add deserialisers for old core types to new type
[CHANGED] moved most of the object specific deserialization
    functions from util.deserialise into the modules of the objects
    being inflated.

[CHANGED] rationalised redundant deserialiser registrations, using
    the fact that a single function can register for multiple types.

[NEW] Registered deserialization functions for migrating from the
     following old type to new type classes: ArrayAlignment,
     Alignment, SequenceCollection, Sequence, MolType, CharAlphabet,
     Alphabet (resolved as CharAlphabet if motif length is 1
     or KmerAlphabet if the number of states is a power of the
     inferred monomer set, or SenseCodonAlphabet), JointEnumeration
     (KmerAlphabet).

[CHANGED] corrected some type hints

## Summary by Sourcery

Streamline deserialization by moving and rationalizing deserialiser functions into object modules, registering mappings for migrating old core types to new classes, and updating tests to validate backward compatibility and roundtrip behavior using the new_type flag

New Features:
- Register deserialisers to migrate legacy rich dict formats of core types—including ArrayAlignment, Alignment, SequenceCollection, Sequence, MolType, CharAlphabet, Alphabet, and JointEnumeration—to their new classes

Enhancements:
- Relocate object-specific deserialisation functions from util.deserialise into their respective modules
- Consolidate redundant deserialiser registrations by allowing single functions to handle multiple provenance types
- Refactor deserialisation code to use get_class in place of internal helpers and correct type hints across modules

Tests:
- Expand and parametrize roundtrip tests to use the new_type flag and cover serialization/migration for moltypes, sequences, alphabets, submodels, likelihoods, trees, and annotations
- Add tests verifying transformation of legacy formats into new core types for alignments, sequence collections, sequences, moltypes, alphabets, substitution models, and likelihood functions


Patch:
diff --git a/noxfile.py b/noxfile.py
index 5056f2091..f17cae1bf 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -96,6 +96,7 @@ def testdocs(session):
             "-1",
             "-s",
             "rst",
+            *session.posargs,
             external=True,
         )
 
diff --git a/src/cogent3/app/composable.py b/src/cogent3/app/composable.py
index 4925d1120..688cf6fc9 100644
--- a/src/cogent3/app/composable.py
+++ b/src/cogent3/app/composable.py
@@ -18,6 +18,7 @@ from cogent3._version import __version__
 from cogent3.app import typing as c3_typing
 from cogent3.util import parallel as PAR
 from cogent3.util import progress_display as UI
+from cogent3.util.deserialise import register_deserialiser
 from cogent3.util.misc import docstring_to_summary_rest, get_object_provenance
 
 from .data_store import (
@@ -953,3 +954,13 @@ __mapping = {
     "as_completed": _as_completed,
     "set_logger": _set_logger,
 }
+
+
+@register_deserialiser(get_object_provenance(NotCompleted))
+def deserialise_not_completed(data: dict) -> NotCompleted:
+    """deserialising NotCompletedResult"""
+    data.pop("version", None)
+    init = data.pop("not_completed_construction")
+    args = init.pop("args")
+    kwargs = init.pop("kwargs")
+    return NotCompleted(*args, **kwargs)
diff --git a/src/cogent3/app/result.py b/src/cogent3/app/result.py
index 6881d2857..f04044f9b 100644
--- a/src/cogent3/app/result.py
+++ b/src/cogent3/app/result.py
@@ -12,6 +12,11 @@ import cogent3
 from cogent3._version import __version__
 from cogent3.app.data_store import get_data_source
 from cogent3.core.table import Table
+from cogent3.util.deserialise import (
+    deserialise_object,
+    get_class,
+    register_deserialiser,
+)
 from cogent3.util.misc import extend_docstring_from, get_object_provenance
 
 
@@ -648,3 +653,23 @@ class tabular_result(generic_result):
 
     def __init__(self, source=None) -> None:
         super().__init__(source)
+
+
+@register_deserialiser("cogent3.app.result")
+def deserialise_result(data):
+    """returns a result object"""
+    data.pop("version", None)
+    klass = get_class(data.pop("type"))
+    kwargs = data.pop("result_construction")
+    result = klass(**kwargs)
+    items = data.pop("items") if "items" in data else data.items()
+    for key, value in items:
+        # only deserialise the result object, other attributes loaded as
+        # required
+        if type(value) == dict and "app.result" in str(value.get("type")):
+            value = deserialise_object(value)
+        try:
+            result[key] = value
+        except TypeError:
+            result[tuple(key)] = value
+    return result
diff --git a/src/cogent3/core/annotation_db.py b/src/cogent3/core/annotation_db.py
index 42d3c3d61..3dedb6fc7 100644
--- a/src/cogent3/core/annotation_db.py
+++ b/src/cogent3/core/annotation_db.py
@@ -15,7 +15,7 @@ import numpy
 import typing_extensions
 
 from cogent3._version import __version__
-from cogent3.core.location import Strand
+from cogent3.core.location import Strand, deserialise_map_spans
 from cogent3.core.table import Table
 from cogent3.parse.gff import merged_gff_records
 from cogent3.util.deserialise import deserialise_object, register_deserialiser
@@ -1732,8 +1732,6 @@ def deserialise_gb_db(data: dict) -> GenbankAnnotationDb:
 
 @register_deserialiser("annotation_to_annotation_db")
 def convert_annotation_to_annotation_db(data: dict) -> SupportsFeatures:
-    from cogent3.util.deserialise import deserialise_map_spans
-
     db = BasicAnnotationDb()
 
     minus = Strand.MINUS
diff --git a/src/cogent3/core/location.py b/src/cogent3/core/location.py
index a90bcff5a..d63450870 100644
--- a/src/cogent3/core/location.py
+++ b/src/cogent3/core/location.py
@@ -16,7 +16,7 @@ import typing_extensions
 from numpy.typing import NDArray
 
 from cogent3._version import __version__
-from cogent3.util.deserialise import register_deserialiser
+from cogent3.util.deserialise import get_class, register_deserialiser
 from cogent3.util.misc import get_object_provenance
 
 strip = str.strip
@@ -1524,11 +1524,9 @@ class IndelMap(MapABC):
 
     @classmethod
     def from_rich_dict(cls, map_element) -> "IndelMap":
-        from cogent3.util.deserialise import _get_class
-
         map_element.pop("version", None)
-        type_ = map_element.pop("type")
-        assert _get_class(type_) == cls
+        type_ = map_element.pop("type", None)
+        assert get_class(type_) == cls
         map_element["gap_pos"] = numpy.array(map_element["gap_pos"])
         map_element["cum_gap_lengths"] = numpy.array(map_element["cum_gap_lengths"])
 
@@ -1693,7 +1691,7 @@ class IndelMap(MapABC):
         )
 
     @property
-    def array(self):
+    def array(self) -> numpy.ndarray:
         """returns 2D numpy array with columns gap position and cum gap lengths"""
         return numpy.array([self.gap_pos, self.cum_gap_lengths]).T
 
@@ -2090,15 +2088,15 @@ class FeatureMap(MapABC):
 
     @classmethod
     def from_rich_dict(cls, map_element) -> "FeatureMap":
-        from cogent3.util.deserialise import _get_class
+        from cogent3.util.deserialise import get_class
 
         map_element.pop("version", None)
         type_ = map_element.pop("type")
-        assert _get_class(type_) == cls
+        assert get_class(type_) == cls
         spans = []
         for element in map_element.pop("spans"):
             element.pop("version", None)
-            klass = _get_class(element.pop("type"))
+            klass = get_class(element.pop("type"))
             instance = klass(**element)
             spans.append(instance)
 
@@ -2117,7 +2115,6 @@ class FeatureMap(MapABC):
         """
         # TODO there's probably a more efficient way to do this
         # create the new instance
-        from cogent3.util.deserialise import deserialise_map_spans
 
         data = self.to_rich_dict()
         zeroed = deserialise_map_spans(data)
@@ -2252,3 +2249,24 @@ def _(index: FeatureMap, length) -> tuple[int, int, int | None]:
     start = _norm_index(index.start, length, 0)
     end = _norm_index(index.end, length, length)
     return start, end, None
+
+
+def deserialise_map_spans(map_element):
+    map_element.pop("version", None)
+    type_ = map_element.pop("type")
+    if type_.endswith(".Map"):
+        # old style Map
+        type_ = type_.replace(".Map", ".FeatureMap")
+        for k in ("tidy", "termini_unknown"):
+            map_element.pop(k, None)
+
+    map_klass = get_class(type_)
+    spans = []
+    for element in map_element["spans"]:
+        element.pop("version", None)
+        klass = get_class(element.pop("type"))
+        instance = klass(**element)
+        spans.append(instance)
+
+    map_element["spans"] = spans
+    return map_klass(**map_element)
diff --git a/src/cogent3/core/new_alignment.py b/src/cogent3/core/new_alignment.py
index b7114f09f..0f3b696f6 100644
--- a/src/cogent3/core/new_alignment.py
+++ b/src/cogent3/core/new_alignment.py
@@ -1755,7 +1755,6 @@ class SequenceCollection(AnnotatableMixin):
                 motif_length=motif_length,
                 include_ambiguity=include_ambiguity,
                 allow_gap=allow_gap,
-                exclude_unobserved=exclude_unobserved,
                 warn=warn,
             )
             motifs.update(c.keys())
@@ -2472,11 +2471,37 @@ class SequenceCollection(AnnotatableMixin):
         return self.take_seqs(omit, negate=True)
 
 
-@register_deserialiser(get_object_provenance(SequenceCollection))
-def deserialise_sequence_collection(data: dict) -> SequenceCollection:
+@register_deserialiser(
+    get_object_provenance(SequenceCollection),
+    "cogent3.core.alignment.SequenceCollection",
+)
+def deserialise_sequence_collection(
+    data: dict[str, str | dict[str, str]],
+) -> SequenceCollection:
+    """deserialise SequenceCollection"""
+    if "init_args" not in data:
+        return deserialise_old_to_new_type_seqcoll(data)
+
     return SequenceCollection.from_rich_dict(data)
 
 
+def deserialise_old_to_new_type_seqcoll(
+    data: dict[str, str | dict[str, str]],
+) -> SequenceCollection:
+    """deserialise old type SequenceCollection as a new type collection"""
+    moltype_name = data["moltype"]
+    moltype_name = "text" if moltype_name == "bytes" else moltype_name
+    info_data = data["info"]
+    source = info_data.pop("source", None)
+    seq_data = {
+        seqid: record["seq"]["init_args"]["seq"]
+        for seqid, record in data["seqs"].items()
+    }
+    return make_unaligned_seqs(
+        seq_data, moltype=moltype_name, info=info_data, source=source
+    )
+
+
 @singledispatch
 def merged_db_collection(seqs) -> SupportsFeatures:
     """return one AnnotationDb from a collection of sequences
@@ -2920,8 +2945,7 @@ def _(
         raise ValueError(msg)
 
     info = info if isinstance(info, dict) else {}
-    source = str(source) if source else str(info.get("source", "unknown"))
-    info["source"] = source
+    source = str(source) if source else str(info.pop("source", "unknown"))
     seqs = SequenceCollection(
         seqs_data=data,
         moltype=moltype,
@@ -5044,7 +5068,6 @@ class Alignment(SequenceCollection):
                 motif_length=motif_length,
                 include_ambiguity=include_ambiguity,
                 allow_gap=allow_gap,
-                exclude_unobserved=exclude_unobserved,
             )
             motifs.update(c.keys())
             counts.append(c)
@@ -6986,7 +7009,7 @@ class Alignment(SequenceCollection):
         }
 
     @classmethod
-    def from_rich_dict(cls, data: dict[str, str | dict[str, str]]):
+    def from_rich_dict(cls, data: dict[str, str | dict[str, str]]) -> Alignment:
         data["init_args"].pop("annotation_db", None)
         return make_aligned_seqs(data["seqs"], **data["init_args"])
 
@@ -7023,11 +7046,67 @@ class Alignment(SequenceCollection):
         return super().duplicated_seqs()
 
 
-@register_deserialiser(get_object_provenance(Alignment))
+@register_deserialiser(
+    get_object_provenance(Alignment), "cogent3.core.alignment.Alignment"
+)
 def deserialise_alignment(data: dict[str, str | dict[str, str]]) -> Alignment:
+    if "init_args" not in data:
+        # old type Alignment class
+        return deserialise_alignment_to_new_type_alignment(data)
     return Alignment.from_rich_dict(data)
 
 
+@register_deserialiser("cogent3.core.alignment.ArrayAlignment")
+def deserialise_array_align_to_new_type_alignment(
+    data: dict[str, str | dict[str, str]],
+) -> Alignment:
+    """deserialise old type ArrayAlignment as a new type alignment."""
+    moltype_name = data["moltype"]
+    moltype_name = "text" if moltype_name == "bytes" else moltype_name
+    info_data = data["info"]
+    source = info_data.pop("source", None) if info_data else None
+
+    seq_data = {}
+    for seqid, record in data["seqs"].items():
+        if isinstance(record["seq"], str):
+            seq = record["seq"]
+        else:
+            seq = record["seq"]["init_args"]["seq"]
+
+        seq_data[seqid] = seq
+
+    return make_aligned_seqs(
+        seq_data, moltype=moltype_name, info=info_data, source=source
+    )
+
+
+def deserialise_alignment_to_new_type_alignment(
+    data: dict[str, str | dict[str, str]],
+) -> Alignment:
+    """deserialise old type Alignment as a new type alignment"""
+    from cogent3 import get_moltype
+    from cogent3.core.location import deserialise_indelmap
+
+    moltype_name = data["moltype"]
+    mt = get_moltype(moltype_name, new_type=True)
+    alpha = mt.most_degen_alphabet()
+    info_data = data["info"]
+    source = info_data.pop("source", None)
+
+    seqs = {}
+    gaps = {}
+    for name, record in data["seqs"].items():
+        seq_data = record["seq_init"]
+        minit = record["map_init"]
+        imap = deserialise_indelmap(minit)
+        raw_seq = seq_data["seq"]["init_args"]["seq"]
+        seqs[name] = raw_seq
+        gaps[name] = imap.array
+
+    asd = AlignedSeqsData.from_seqs_and_gaps(seqs=seqs, gaps=gaps, alphabet=alpha)
+    return make_aligned_seqs(asd, moltype=moltype_name, info=info_data, source=source)
+
+
 def make_aligned_storage(
     data: dict[str, bytes | numpy.ndarray[int]],
     *,
@@ -7207,9 +7286,7 @@ def _(
         raise ValueError(msg)
 
     info = info if isinstance(info, dict) else {}
-    source = str(source) if source else str(info.get("source", "unknown"))
-    info["source"] = source
-
+    source = str(source) if source else str(info.pop("source", "unknown"))
     sr = (
         new_sequence.SliceRecord(parent_len=data.align_len, step=-1)
         if is_reversed
diff --git a/src/cogent3/core/new_alphabet.py b/src/cogent3/core/new_alphabet.py
index 96367b63a..f4616961a 100644
--- a/src/cogent3/core/new_alphabet.py
+++ b/src/cogent3/core/new_alphabet.py
@@ -704,9 +704,33 @@ def make_converter(
     )
 
 
-@register_deserialiser(get_object_provenance(CharAlphabet))
+def _get_closest_char_alphabet(
+    moltype_name: str, motifset: list[str] | set[str]
+) -> CharAlphabet:
+    from cogent3.core.new_moltype import get_moltype
+
+    mtype = get_moltype(moltype_name)
+    motifset = set(motifset)
+    num_motifs = len(motifset)
+    alphas = sorted([(abs(len(a) - num_motifs), a) for a in mtype.iter_alphabets()])
+    return alphas[0][1]
+
+
+@register_deserialiser(
+    get_object_provenance(CharAlphabet), "cogent3.core.alphabet.CharAlphabet"
+)
 def deserialise_char_alphabet(data: dict) -> CharAlphabet:
-    return CharAlphabet.from_rich_dict(data)
+    if "motifset" not in data:
+        return CharAlphabet.from_rich_dict(data)
+
+    from cogent3.core.new_moltype import get_moltype
+
+    # this is a legacy format, we assume this was derived from a
+    # moltype so we get the moltype and look for an alphabet with
+    # matching characters and gap
+    mtype = get_moltype(data["moltype"])
+    motifset = set(data.pop("motifset"))
+    return _get_closest_char_alphabet(mtype, motifset)
 
 
 @numba.jit(nopython=True)
@@ -1195,9 +1219,19 @@ class KmerAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
         return self.k
 
 
-@register_deserialiser(get_object_provenance(KmerAlphabet))
+@register_deserialiser(
+    get_object_provenance(KmerAlphabet), "cogent3.core.alphabet.JointEnumeration"
+)
 def deserialise_kmer_alphabet(data: dict) -> KmerAlphabet:
-    return KmerAlphabet.from_rich_dict(data)
+    if data["type"] != "cogent3.core.alphabet.JointEnumeration":
+        return KmerAlphabet.from_rich_dict(data)
+
+    monomers = data["data"][0]
+    motif_length = len(data["data"])
+    # which is the closest char alphabet
+    mtype = data["moltype"]
+    alpha = _get_closest_char_alphabet(mtype, monomers)
+    return alpha.get_kmer_alphabet(motif_length, include_gap=bool(data["gap"]))
 
 
 class SenseCodonAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
@@ -1409,9 +1443,50 @@ class SenseCodonAlphabet(tuple, AlphabetABC, KmerAlphabetABC):
         return self._motif_len
 
 
+@register_deserialiser("cogent3.core.alphabet.Alphabet")
+def deserialise_old_alphabet(data: dict) -> KmerAlphabet | SenseCodonAlphabet:
+    motif_length = len(data["motifset"][0])
+    if motif_length == 1:
+        # this is a char alphabet
+        return deserialise_char_alphabet(data)
+
+    motifs = set(data["motifset"])
+    num_motifs = len(motifs)
+    num_monomers = round(num_motifs ** (1 / motif_length))
+    if num_monomers**motif_length != num_motifs:
+        # if the number of motifs is not the motif_length exponent of the number
+        # of monomers, we have a codon alphabet
+        return deserialise_codon_alphabet(data)
+
+    # we get the closest alphabet
+    mtype = data["moltype"]
+    monomers = list(set("".join(motifs)))
+    alpha = _get_closest_char_alphabet(mtype, monomers)
+    return alpha.get_kmer_alphabet(motif_length, include_gap=bool(data["gap"]))
+
+
 @register_deserialiser(get_object_provenance(SenseCodonAlphabet))
 def deserialise_codon_alphabet(data: dict) -> SenseCodonAlphabet:
-    return SenseCodonAlphabet.from_rich_dict(data)
+    if data["type"] != "cogent3.core.alphabet.Alphabet":
+        return SenseCodonAlphabet.from_rich_dict(data)
+
+    # we search through the genetic codes for the one with a matching
+    # motifset
+    from cogent3.core.new_genetic_code import available_codes, get_code
+
+    include_gap = bool(data["gap"])
+    motifs = set(data["motifset"])
+    alphabets = []
+    code_ids = available_codes().columns["Code ID"]
+    for code_id in sorted(code_ids):
+        code = get_code(code_id)
+        code_alpha = code.get_alphabet(include_gap=include_gap)
+        diff = len(set(code_alpha) ^ motifs)
+        alphabets.append((diff, code))
+
+    alphabets = sorted(alphabets, key=lambda x: x[0])
+    code = alphabets[0][1]
+    return code.get_alphabet(include_gap=include_gap)
 
 
 _alphabet_moltype_map = {}
diff --git a/src/cogent3/core/new_moltype.py b/src/cogent3/core/new_moltype.py
index cc7cc4f53..f87dc1900 100644
--- a/src/cogent3/core/new_moltype.py
+++ b/src/cogent3/core/new_moltype.py
@@ -1388,9 +1388,9 @@ def _most_degen_alphabet(mt: MolType) -> new_alphabet.CharAlphabet:
     return next(mt.iter_alphabets())
 
 
-@register_deserialiser(get_object_provenance(MolType))
-def deserialise_new_moltype(data, **kwargs):
-    return get_moltype(data.pop("moltype"))
+@register_deserialiser(get_object_provenance(MolType), "cogent3.core.moltype.MolType")
+def deserialise_new_moltype(data: dict[str, str]) -> MolType:
+    return get_moltype(data["moltype"])
 
 
 def _make_moltype_dict() -> dict[str, MolType]:
diff --git a/src/cogent3/core/new_sequence.py b/src/cogent3/core/new_sequence.py
index 4934b57ac..835549e32 100644
--- a/src/cogent3/core/new_sequence.py
+++ b/src/cogent3/core/new_sequence.py
@@ -73,7 +73,10 @@ frac_same = for_seq(f=eq, aggregator=sum, normalizer=per_shortest)
 frac_diff = for_seq(f=ne, aggregator=sum, normalizer=per_shortest)
 
 
-def _moltype_seq_from_rich_dict(data):
+def _moltype_seq_from_rich_dict(
+    data: dict[str, str | dict[str, str]],
+) -> tuple[new_moltype.MolType, StrORBytesORArray]:
+    """returns moltype and seq and mutates data so it can serve as kwargs to Sequence constructor"""
     from cogent3.core import new_moltype
 
     data.pop("type")
@@ -291,8 +294,12 @@ class Sequence(AnnotatableMixin):
         return data
 
     @classmethod
-    def from_rich_dict(cls, data: dict):
+    def from_rich_dict(cls, data: dict) -> Sequence:
         """create a Sequence object from a rich dict"""
+        if isinstance(data["seq"], dict):
+            # this is a rich dict from the old type sequences
+            data["seq"] = data.pop("seq")["init_args"]["seq"]
+
         moltype, seq = _moltype_seq_from_rich_dict(data)
 
         return cls(moltype=moltype, seq=seq, **data)
@@ -301,16 +308,18 @@ class Sequence(AnnotatableMixin):
         """returns a json formatted string"""
         return json.dumps(self.to_rich_dict())
 
-    def count(self, item: str):
+    def count(self, item: str) -> int:
         """count() delegates to self._seq."""
         return str(self).count(item)
 
+    @c3warn.deprecated_args(
+        "2025.9", reason="has no effect", discontinued="exclude_unobserved"
+    )
     def counts(
         self,
         motif_length: int = 1,
         include_ambiguity: bool = False,
         allow_gap: bool = False,
-        exclude_unobserved: bool = False,
         warn: bool = False,
     ) -> CategoryCounter:
         """returns dict of counts of motifs
@@ -326,8 +335,6 @@ class Sequence(AnnotatableMixin):
             from the seq moltype are included. No expansion of those is attempted.
         allow_gaps
             if True, motifs containing a gap character are included.
-        exclude_unobserved
-            if True, unobserved motif combinations are excluded.
         warn
             warns if motif_length > 1 and alignment trimmed to produce
             motif columns
@@ -380,21 +387,21 @@ class Sequence(AnnotatableMixin):
         """Returns the number of ambiguous characters in the sequence."""
         data = numpy.array(self)
         gap_index = self.moltype.most_degen_alphabet().gap_index
-        return numpy.sum(data > gap_index)
+        return int(numpy.sum(data > gap_index))
 
-    def __lt__(self, other):
+    def __lt__(self, other) -> bool:
         """compares based on the sequence string."""
         return str(self) < str(other)
 
-    def __eq__(self, other):
+    def __eq__(self, other) -> bool:
         """compares based on the sequence string."""
         return str(self) == str(other)
 
-    def __ne__(self, other):
+    def __ne__(self, other) -> bool:
         """compares based on the sequence string."""
         return str(self) != str(other)
 
-    def __hash__(self):
+    def __hash__(self) -> int:
         """__hash__ behaves like the sequence string for dict lookup."""
         return hash(str(self))
 
@@ -1875,44 +1882,24 @@ class Sequence(AnnotatableMixin):
         )
 
 
-@register_deserialiser(get_object_provenance(Sequence))
-def deserialise_sequence(data: dict) -> Sequence:
-    return Sequence.from_rich_dict(data)
-
-
 class ProteinSequence(Sequence):
     """Holds the standard Protein sequence."""
 
     # constructed by PROTEIN moltype
 
 
-@register_deserialiser(get_object_provenance(ProteinSequence))
-def deserialise_protein_sequence(data) -> ProteinSequence:
-    return ProteinSequence.from_rich_dict(data)
-
-
 class ByteSequence(Sequence):
     """Holds the bytes sequence."""
 
     # constructed by BYTES moltype
 
 
-@register_deserialiser(get_object_provenance(ByteSequence))
-def deserialise_bytes_sequence(data) -> ByteSequence:
-    return ByteSequence.from_rich_dict(data)
-
-
 class ProteinWithStopSequence(Sequence):
     """Holds the standard Protein sequence, allows for stop codon."""
 
     # constructed by PROTEIN_WITH_STOP moltype
 
 
-@register_deserialiser(get_object_provenance(ProteinWithStopSequence))
-def deserialise_protein_with_stop_sequence(data) -> ProteinWithStopSequence:
-    return ProteinWithStopSequence.from_rich_dict(data)
-
-
 class NucleicAcidSequenceMixin:
     """Mixin class for DNA and RNA sequences."""
 
@@ -2184,22 +2171,12 @@ class DnaSequence(Sequence, NucleicAcidSequenceMixin):
     # constructed by DNA moltype
 
 
-@register_deserialiser(get_object_provenance(DnaSequence))
-def deserialise_dna_sequence(data) -> DnaSequence:
-    return DnaSequence.from_rich_dict(data)
-
-
 class RnaSequence(Sequence, NucleicAcidSequenceMixin):
     """Holds the standard RNA sequence."""
 
     # constructed by RNA moltype
 
 
-@register_deserialiser(get_object_provenance(RnaSequence))
-def deserialise_rna_sequence(data) -> RnaSequence:
-    return RnaSequence.from_rich_dict(data)
-
-
 class SliceRecordABC(ABC):
     """Abstract base class for recording the history of operations to be applied
     to some underlying data. Provides slicing functionality for the underlying data.
@@ -3032,7 +3009,7 @@ class SeqView(SeqViewABC):
 def _coerce_to_seqview(
     data,
     seqid: str,
-    alphabet: new_alphabet.AlphabetABC,
+    alphabet: new_alphabet.CharAlphabet,
     offset: int,
 ) -> SeqViewABC:
     from cogent3.core.alignment import Aligned
@@ -3049,7 +3026,7 @@ def _coerce_to_seqview(
 def _(
     data: SeqViewABC,
     seqid: str,
-    alphabet: new_alphabet.AlphabetABC,
+    alphabet: new_alphabet.CharAlphabet,
     offset: int,
 ) -> SeqViewABC:
     # we require the indexes of shared states in alphabets to be the same
@@ -3078,7 +3055,7 @@ def _(
 def _(
     data: Sequence,
     seqid: str,
-    alphabet: new_alphabet.AlphabetABC,
+    alphabet: new_alphabet.CharAlphabet,
     offset: int,
 ) -> SeqViewABC:
     return _coerce_to_seqview(data._seq, seqid, alphabet, offset)
@@ -3088,7 +3065,7 @@ def _(
 def _(
     data: str,
     seqid: str,
-    alphabet: new_alphabet.AlphabetABC,
+    alphabet: new_alphabet.CharAlphabet,
     offset: int,
 ) -> SeqViewABC:
     return SeqView(
@@ -3104,7 +3081,7 @@ def _(
 def _(
     data: bytes,
     seqid: str,
-    alphabet: new_alphabet.AlphabetABC,
+    alphabet: new_alphabet.CharAlphabet,
     offset: int,
 ) -> SeqViewABC:
     data = data.decode("utf8")
@@ -3151,3 +3128,29 @@ def _(
     offset: int,
 ) -> SeqViewABC:
     return _coerce_to_seqview("".join(data), seqid, alphabet, offset)
+
+
+cls_map = {
+    get_object_provenance(cls): cls
+    for cls in (
+        Sequence,
+        DnaSequence,
+        RnaSequence,
+        ProteinSequence,
+        ProteinWithStopSequence,
+        ByteSequence,
+    )
+} | {
+    "cogent3.core.sequence.Sequence": Sequence,
+    "cogent3.core.sequence.DnaSequence": DnaSequence,
+    "cogent3.core.sequence.RnaSequence": RnaSequence,
+    "cogent3.core.sequence.ProteinSequence": ProteinSequence,
+    "cogent3.core.sequence.ProteinWithStopSequence": ProteinWithStopSequence,
+    "cogent3.core.sequence.ByteSequence": ByteSequence,
+}
+
+
+@register_deserialiser(*cls_map.keys())
+def deserialise_sequence(data: dict) -> Sequence:
+    cls = cls_map[data["type"]]
+    return cls.from_rich_dict(data)
diff --git a/src/cogent3/core/table.py b/src/cogent3/core/table.py
index 014ed6782..b21163a91 100644
--- a/src/cogent3/core/table.py
+++ b/src/cogent3/core/table.py
@@ -27,6 +27,7 @@ from cogent3.format import bedgraph
 from cogent3.format import table as table_format
 from cogent3.parse import cogent3_json as c3_json
 from cogent3.parse.table import load_delimited
+from cogent3.util.deserialise import register_deserialiser
 from cogent3.util.dict_array import DictArray, DictArrayTemplate
 from cogent3.util.io import atomic_write, get_format_suffixes, open_
 from cogent3.util.misc import extend_docstring_from, get_object_provenance
@@ -2469,3 +2470,20 @@ def load_table(
         legend=legend,
         format=format,
     )
+
+
+@register_deserialiser(
+    get_object_provenance(Table),
+    "cogent3.util.table.Table",
+)
+def deserialise_tabular(data: dict) -> Table:
+    """deserialising DictArray, Table instances"""
+    data.pop("version", None)
+    _ = data.pop("type", None)
+    if "init_table" in data:
+        result = Table()
+        result.__setstate__(data)
+    else:
+        result = Table(**data)
+
+    return result
diff --git a/src/cogent3/core/tree.py b/src/cogent3/core/tree.py
index 5e720fbb4..eee687938 100644
--- a/src/cogent3/core/tree.py
+++ b/src/cogent3/core/tree.py
@@ -47,6 +47,7 @@ from cogent3.parse.newick import parse_string as newick_parse_string
 from cogent3.parse.tree_xml import parse_string as tree_xml_parse_string
 from cogent3.phylo.tree_distance import get_tree_distance_measure
 from cogent3.util import warning as c3warn
+from cogent3.util.deserialise import register_deserialiser
 from cogent3.util.io import atomic_write, get_format_suffixes, open_
 from cogent3.util.misc import get_object_provenance, is_number
 
@@ -2395,3 +2396,17 @@ def load_tree(
         underscore_unmunge=underscore_unmunge,
         source=filename,
     )
+
+
+@register_deserialiser("cogent3.core.tree")
+def deserialise_tree(
+    data: dict[str, str | dict[str, str | float | None]],
+) -> PhyloNode | TreeNode:
+    """returns a cogent3 PhyloNode instance"""
+    # we load tree using make_tree, then populate edge attributes
+    edge_attr = data["edge_attributes"]
+    tree = make_tree(treestring=data["newick"])
+    for edge in tree.preorder():
+        params = edge_attr.get(edge.name, {})
+        edge.params.update(params)
+    return tree
diff --git a/src/cogent3/evolve/fast_distance.py b/src/cogent3/evolve/fast_distance.py
index c340a4ec1..f184ceb83 100644
--- a/src/cogent3/evolve/fast_distance.py
+++ b/src/cogent3/evolve/fast_distance.py
@@ -11,6 +11,7 @@ import cogent3
 from cogent3._version import __version__
 from cogent3.core import new_moltype
 from cogent3.core.table import Table
+from cogent3.util.deserialise import register_deserialiser
 from cogent3.util.dict_array import DictArray
 from cogent3.util.misc import get_object_provenance
 from cogent3.util.progress_display import display_wrap
@@ -912,3 +913,18 @@ class DistanceMatrix(DictArray):
         min_index_flat = numpy.argmin(dmat_copy)
         min_index_1, min_index_2 = numpy.unravel_index(min_index_flat, self.shape)
         return self.names[min_index_1], self.names[min_index_2]
+
+
+@register_deserialiser(get_object_provenance(DistanceMatrix))
+def deserialise_tabular(data: dict) -> DistanceMatrix:
+    """deserialising DistanceMatrix from a rich dict"""
+    data.pop("version", None)
+    _ = data.pop("type", None)
+    # dists is a list of simple dists from which we reconstruct a 1D dict
+    dists = {}
+    for element in data["dists"]:
+        key = tuple(element[:2])
+        value = element[2]
+        dists[key] = value
+    data["dists"] = dists
+    return DistanceMatrix(**data)
diff --git a/src/cogent3/evolve/models.py b/src/cogent3/evolve/models.py
index 0d422aec4..e84ebd3f7 100644
--- a/src/cogent3/evolve/models.py
+++ b/src/cogent3/evolve/models.py
@@ -7,6 +7,7 @@ gap recoding that will convert gaps to Ns, and model gaps set to False."""
 
 # this file using functions etc. to allow each model to serve as an example for users
 # wishing to construct their own models
+import contextlib
 from itertools import permutations
 
 # The models are constructed in a strait forward manner with no attempt to condense
@@ -20,6 +21,11 @@ from cogent3.evolve.solved_models import _solved_nucleotide
 from cogent3.evolve.substitution_model import _SubstitutionModel
 from cogent3.evolve.substitution_model import kappa_r as _kappa_r
 from cogent3.evolve.substitution_model import kappa_y as _kappa_y
+from cogent3.util.deserialise import (
+    deserialise_object,
+    get_class,
+    register_deserialiser,
+)
 
 nucleotide_models = []
 codon_models = []
@@ -2864,3 +2870,29 @@ def available_models(model_types=None):
         data=rows,
         title="Specify a model using 'Abbreviation' (case sensitive).",
     )
+
+
+@register_deserialiser(
+    "cogent3.evolve.substitution_model",
+    "cogent3.evolve.ns_substitution_model",
+    "cogent3.evolve.solved_models.PredefinedNucleotide",
+)
+def deserialise_substitution_model(data):
+    """returns a cogent3 substitution model instance"""
+
+    data.pop("version", None)
+    kw = {} if "kw" not in data else data.pop("kw")
+    sm = None
+    if kw and "name" in kw:
+        name = kw.pop("name")
+        with contextlib.suppress(ValueError):
+            # user defined sm?
+            sm = get_model(name, **kw)
+
+    if sm is None:
+        alphabet = deserialise_object(data.pop("alphabet"))
+        klass = get_class(data.pop("type"))
+        data["alphabet"] = alphabet
+        sm = klass(**data)
+
+    return sm
diff --git a/src/cogent3/util/deserialise.py b/src/cogent3/util/deserialise.py
index 288dddd64..4a9f1ce22 100644
--- a/src/cogent3/util/deserialise.py
+++ b/src/cogent3/util/deserialise.py
@@ -2,7 +2,6 @@ import json
 import re
 from importlib import import_module
 
-import cogent3
 from cogent3.util.io import open_, path_exists
 
 _deserialise_func_map = {}
@@ -39,7 +38,7 @@ class register_deserialiser:
         return func
 
 
-def _get_class(provenance):
+def get_class(provenance: str) -> type:
     index = provenance.rfind(".")
     assert index > 0
     klass = provenance[index + 1 :]
@@ -57,289 +56,12 @@ def str_to_version(v):
     return tuple(f"{v[: letter.start()]}.{letter.group()}.{letter.end():}".split("."))
 
 
-@register_deserialiser(
-    "cogent3.core.table.Table",
-    "cogent3.util.dict_array.DictArray",
-    "cogent3.evolve.fast_distance.DistanceMatrix",
-)
-def deserialise_tabular(data):
-    """deserialising DictArray, Table instances"""
-    data.pop("version", None)
-    type_ = data.pop("type")
-    klass = _get_class(type_)
-    if type_.endswith("Table"):
-        if "init_table" in data:
-            result = klass()
-            result.__setstate__(data)
-        else:
-            result = klass(**data)
-    elif "dictarray" in type_.lower():
-        named_dims = data.pop("names")
-        array = data.pop("array")
-        template = klass(*named_dims)
-        result = template.wrap(array)
-    else:  # DistanceMatrix
-        # dists is a list of simple dists from which we reconstruct a 1D dict
-        dists = {}
-        for element in data["dists"]:
-            key = tuple(element[:2])
-            value = element[2]
-            dists[key] = value
-        data["dists"] = dists
-        result = klass(**data)
-
-    return result
-
-
-@register_deserialiser("cogent3.core.sequence.SeqView")
-def deserialise_seqview(data):
-    """deserialising SeqView"""
-    klass = _get_class(data["type"])
-    return klass.from_rich_dict(data)
-
-
-@register_deserialiser("cogent3.app.composable.NotCompleted")
-def deserialise_not_completed(data):
-    """deserialising NotCompletedResult"""
-    data.pop("version", None)
-    klass = _get_class(data.pop("type"))
-    init = data.pop("not_completed_construction")
-    args = init.pop("args")
-    kwargs = init.pop("kwargs")
-    return klass(*args, **kwargs)
-
-
-def deserialise_map_spans(map_element):
-    map_element.pop("version", None)
-    type_ = map_element.pop("type")
-    if type_.endswith(".Map"):
-        # old style Map
-        type_ = type_.replace(".Map", ".FeatureMap")
-        for k in ("tidy", "termini_unknown"):
-            map_element.pop(k, None)
-
-    map_klass = _get_class(type_)
-    spans = []
-    for element in map_element["spans"]:
-        element.pop("version", None)
-        klass = _get_class(element.pop("type"))
-        instance = klass(**element)
-        spans.append(instance)
-
-    map_element["spans"] = spans
-    return map_klass(**map_element)
-
-
-def deserialise_annotation(data, parent) -> None:
-    annots = []
-    for element in data:
-        element.pop("version", None)
-        annotations = element.pop("annotations", None)
-        klass = _get_class(element.pop("type"))
-        kwargs = element.pop("annotation_construction")
-        kwargs["map"] = deserialise_map_spans(kwargs["map"])
-        instance = klass(parent, **kwargs)
-        if annotations:
-            deserialise_annotation(annotations, instance)
-        annots.append(instance)
-    parent.annotations += tuple(annots)
-
-
-@register_deserialiser("cogent3.app.result")
-def deserialise_result(data):
-    """returns a result object"""
-    data.pop("version", None)
-    klass = _get_class(data.pop("type"))
-    kwargs = data.pop("result_construction")
-    result = klass(**kwargs)
-    items = data.pop("items") if "items" in data else data.items()
-    for key, value in items:
-        # only deserialise the result object, other attributes loaded as
-        # required
-        if type(value) == dict and "app.result" in str(value.get("type")):
-            value = deserialise_object(value)
-        try:
-            result[key] = value
-        except TypeError:
-            result[tuple(key)] = value
-    return result
-
-
-@register_deserialiser("cogent3.core.moltype")
-def deserialise_moltype(data):
-    """returns a cogent3 MolType instance, or a CodonAlphabet"""
-    from cogent3 import get_moltype
-
-    data.pop("version", None)
-    label = data["moltype"]
-    data["moltype"] = get_moltype(label)
-    _get_class(data.pop("type"))
-    return data["moltype"]
-
-
-@register_deserialiser("cogent3.core.alphabet")
-def deserialise_alphabet(data):
-    """returns a cogent3 Alphabet instance"""
-    from cogent3 import get_moltype
-
-    data.pop("version", None)
-    label = data["moltype"]
-    data["moltype"] = get_moltype(label)
-    key = "data" if "data" in data else "motifset"
-    motifs = data.pop(key)
-    klass = _get_class(data.pop("type"))
-    return klass(motifs, **data)
-
-
-def _from_seqview(data):
-    data["seq"] = deserialise_seqview(data["seq"])
-    return data
-
-
-@register_deserialiser("cogent3.core.alignment.Aligned")
-def deserialise_aligned(data):
-    klass = _get_class(data.pop("type"))
-    return klass.from_rich_dict(data)
-
-
-@register_deserialiser("cogent3.core.sequence")
-def deserialise_seq(data, aligned=False):
-    """deserialises sequence and any annotations
-
-    Parameters
-    ----------
-    data : dict
-        a result of json.loads of a to_rich_dict()
-    aligned
-        whether sequence type is for an Alignment, in which case an Aligned
-        instance will be returned
-    Returns
-    -------
-
-    """
-    from cogent3 import get_moltype
-    from cogent3.core.alignment import Aligned
-
-    data.pop("version", None)
-    data["moltype"] = get_moltype(data.pop("moltype"))
-    annotations = data.pop("annotations", None)
-    if annotations:
-        annotation_db = {
-            "type": "annotation_to_annotation_db",
-            "data": annotations,
-            "seqid": data.get("name", None),
-        }
-    else:
-        annotation_db = data.pop("annotation_db", None)
-
-    make_seq = data.pop("moltype").make_seq
-    _ = data.pop("type")
-    if "-" in data["seq"]:
-        aligned = True
-
-    if isinstance(data["seq"], dict):
-        data = _from_seqview(data)
-
-    result = make_seq(**data)
-
-    if annotation_db:
-        annotation_db = deserialise_object(annotation_db)
-
-    if aligned:
-        map_, result = result.parse_out_gaps()
-        result = Aligned(map_, result)
-        result.data.annotation_db = annotation_db
-    else:
-        result.annotation_db = annotation_db
-
-    return result
-
-
-@register_deserialiser("cogent3.core.alignment")
-def deserialise_seq_collections(data):
-    """returns a cogent3 sequence/collection/alignment instance"""
-    # We first try to load moltype/alphabet using get_moltype
-    from cogent3 import get_moltype
-
-    data.pop("version", None)
-    data["moltype"] = get_moltype(data.pop("moltype"))
-    annotations = data.pop("annotations", None)
-    if annotations:
-        annotation_db = {
-            "type": "annotation_to_annotation_db",
-            "data": annotations,
-        }
-    else:
-        annotation_db = data.pop("annotation_db", None)
-
-    type_ = data.pop("type")
-    klass = _get_class(type_)
-    assert "alignment" in type_.lower(), "not alignment type"
-    aligned = not type_.endswith("SequenceCollection")
-    seqs = []
-    for v in data.pop("seqs").values():
-        v["moltype"] = data["moltype"]
-        if v["type"].endswith("Aligned"):
-            seq = deserialise_aligned(v)
-        else:
-            seq = deserialise_seq(v, aligned=aligned)
-        seqs.append(seq)
-
-    result = klass(seqs, **data)
-
-    if annotation_db:
-        result.annotation_db = deserialise_object(annotation_db)
-
-    return result
-
-
-@register_deserialiser("cogent3.core.tree")
-def deserialise_tree(data):
-    """returns a cogent3 PhyloNode instance"""
-    data.pop("version", None)
-    # we load tree using make_tree, then populate edge attributes
-    newick = data.pop("newick")
-    edge_attr = data.pop("edge_attributes")
-    tree = cogent3.make_tree(treestring=newick)
-    for edge in tree.get_edge_vector():
-        params = edge_attr.get(edge.name, {})
-        edge.params.update(params)
-    return tree
-
-
-@register_deserialiser(
-    "cogent3.evolve.substitution_model",
-    "cogent3.evolve.ns_substitution_model",
-)
-def deserialise_substitution_model(data):
-    """returns a cogent3 substitution model instance"""
-    from cogent3.evolve.models import get_model
-
-    data.pop("version", None)
-    kw = {} if "kw" not in data else data.pop("kw")
-    sm = None
-    if kw and "name" in kw:
-        name = kw.pop("name")
-        try:
-            sm = get_model(name, **kw)
-        except ValueError:  # user defined sm?
-            pass
-
-    if sm is None:
-        alphabet = deserialise_object(data.pop("alphabet"))
-        klass = _get_class(data.pop("type"))
-        data["alphabet"] = alphabet
-        sm = klass(**data)
-
-    return sm
-
-
 @register_deserialiser("cogent3.evolve.parameter_controller")
 def deserialise_likelihood_function(data):
     """returns a cogent3 likelihood function instance"""
     data.pop("version", None)
-    model = deserialise_substitution_model(data.pop("model"))
-    tree = deserialise_tree(data.pop("tree"))
+    model = deserialise_object(data.pop("model"))
+    tree = deserialise_object(data.pop("tree"))
     constructor_args = data.pop("likelihood_construction")
     motif_probs = data.pop("motif_probs")
     param_rules = data.pop("param_rules")
diff --git a/src/cogent3/util/dict_array.py b/src/cogent3/util/dict_array.py
index f0debbf23..4e437bc15 100644
--- a/src/cogent3/util/dict_array.py
+++ b/src/cogent3/util/dict_array.py
@@ -28,6 +28,7 @@ from itertools import combinations, product
 import numpy
 
 from cogent3._version import __version__
+from cogent3.util.deserialise import get_class, register_deserialiser
 from cogent3.util.io import atomic_write
 from cogent3.util.misc import get_object_provenance
 
@@ -662,3 +663,17 @@ class DictArray:
         data = self.to_string(format=format, sep=sep)
         with atomic_write(path, mode="wt") as outfile:
             outfile.write(data)
+
+
+@register_deserialiser(
+    get_object_provenance(DictArrayTemplate),
+)
+def deserialise_dict_array(data: dict) -> DictArray:
+    """deserialising DictArray, Table instances"""
+    data.pop("version", None)
+    type_ = data.pop("type")
+    klass = get_class(type_)
+    named_dims = data.pop("names")
+    array = data.pop("array")
+    template = klass(*named_dims)
+    return template.wrap(array)


Test patch:
diff --git a/tests/test_app/test_io.py b/tests/test_app/test_io.py
index 9dde6b54d..cd2d5ce5e 100644
--- a/tests/test_app/test_io.py
+++ b/tests/test_app/test_io.py
@@ -30,9 +30,9 @@ from cogent3.maths.util import safe_log
 from cogent3.parse.sequence import PARSERS
 from cogent3.util.deserialise import deserialise_object
 
-DNA = get_moltype("dna")
+NEW_TYPE = True
 
-NEW_TYPE = "COGENT3_NEW_TYPE" in os.environ
+DNA = get_moltype("dna", new_type=NEW_TYPE)
 
 
 @pytest.fixture
@@ -548,6 +548,7 @@ def seqs():
         data={"a": "ACGG", "b": "GGC"},
         moltype="dna",
         info={"source": "dummy/blah.1.2.fa"},
+        new_type=True,
     )
 
 
diff --git a/tests/test_app/test_result.py b/tests/test_app/test_result.py
index 5bc489fe6..aa7259c3e 100644
--- a/tests/test_app/test_result.py
+++ b/tests/test_app/test_result.py
@@ -21,7 +21,7 @@ _NEW_TYPE = "COGENT3_NEW_TYPE" in os.environ
 
 def test_deserialised_values():
     """correctly deserialises values"""
-    DNA = cogent3.get_moltype("dna")
+    DNA = cogent3.get_moltype("dna", new_type=True)
     data = {"type": "cogent3.core.moltype.MolType", "moltype": "dna"}
     result = generic_result(source="blah.json")
     result["key"] = data
@@ -29,7 +29,7 @@ def test_deserialised_values():
     got = result["key"]
     assert got is DNA
     # if we have a type value without "cogent3", leaves as is
-    data = {"type": "core.moltype.MolType", "moltype": "dna"}
+    data = {"type": "core.new_moltype.MolType", "moltype": "dna"}
     result = generic_result(source="blah.json")
     result["key"] = data
     result.deserialised_values()
diff --git a/tests/test_util/test_deserialise.py b/tests/test_util/test_deserialise.py
index 973e0ac81..840eb1999 100644
--- a/tests/test_util/test_deserialise.py
+++ b/tests/test_util/test_deserialise.py
@@ -1,5 +1,4 @@
 import json
-import os
 
 import numpy
 import pytest
@@ -8,6 +7,7 @@ from numpy.testing import assert_allclose
 from cogent3 import (
     get_app,
     get_code,
+    get_moltype,
     load_aligned_seqs,
     load_tree,
     make_aligned_seqs,
@@ -15,7 +15,6 @@ from cogent3 import (
     make_unaligned_seqs,
 )
 from cogent3.app.result import model_collection_result, model_result
-from cogent3.core import moltype
 from cogent3.evolve.models import get_model
 from cogent3.evolve.ns_substitution_model import (
     NonReversibleDinucleotide,
@@ -27,21 +26,21 @@ from cogent3.util.deserialise import (
     deserialise_object,
 )
 
-_NEW_TYPE = "COGENT3_NEW_TYPE" in os.environ
+_NEW_TYPE = True
 
 
 def test_roundtrip_codon_alphabet():
     """codon alphabet to_json enables roundtrip"""
-    STANDARD_CODON = get_code(1).get_alphabet()
-    data = STANDARD_CODON.to_json()
+    standard = get_code(1, new_type=True).get_alphabet()
+    data = standard.to_json()
     got = deserialise_object(data)
-    assert isinstance(got, type(STANDARD_CODON))
-    assert list(got) == list(STANDARD_CODON)
+    assert isinstance(got, type(standard))
+    assert list(got) == list(standard)
 
 
 def test_roundtrip_alphabet():
     """alphabet to_json enables roundtrip"""
-    dna = moltype.get_moltype("dna")
+    dna = get_moltype("dna", new_type=True)
     data = dna.alphabet.to_json()
     got = deserialise_object(data)
     assert isinstance(got, type(dna.alphabet))
@@ -50,12 +49,11 @@ def test_roundtrip_alphabet():
 
 def test_roundtrip_moltype():
     """moltype to_json enables roundtrip"""
-    dna = moltype.get_moltype("dna")
+    dna = get_moltype("dna", new_type=True)
     data = dna.to_json()
     got = deserialise_object(data)
     assert isinstance(got, type(dna))
-    assert list(got) == list(dna)
-    assert dna == got
+    assert got.name == dna.name
 
 
 def test_roundtrip_seqcoll():
@@ -67,15 +65,6 @@ def test_roundtrip_seqcoll():
     assert got.__class__.__name__ == "SequenceCollection"
 
 
-def test_roundtrip_arrayalign():
-    """ArrayAlignment to_json enables roundtrip"""
-    data = {"A": "TTGTA", "B": "GGCT-"}
-    arrayalign = make_aligned_seqs(data=data, moltype="dna")
-    got = deserialise_object(arrayalign.to_json())
-    assert got.rc().to_dict() == arrayalign.rc().to_dict()
-    assert got.__class__.__name__.endswith("Alignment")
-
-
 def test_roundtrip_align():
     """Alignment to_json enables roundtrip"""
     data = {"A": "TTGTA", "B": "GGCT-"}
@@ -93,45 +82,40 @@ def test_roundtrip_tree():
     assert_allclose(got.get_node_matching_name("xx").length, 0.2)
 
 
-def test_roundtrip_submod():
+@pytest.mark.parametrize("model_name", ["HKY85", "GN", "CNFGTR", "GNC"])
+def test_roundtrip_submod(model_name):
     """substitution model to_json enables roundtrip"""
-    sm = get_model("HKY85")
-    data = sm.to_json()
-    got = deserialise_object(data)
-    assert got.to_rich_dict() == sm.to_rich_dict()
-    sm = get_model("GN")
+    sm = get_model(model_name)
     data = sm.to_json()
     got = deserialise_object(data)
-    assert got.to_rich_dict() == sm.to_rich_dict()
-    sm = get_model("CNFGTR")
-    data = sm.to_json()
-    got = deserialise_object(data)
-    assert got.to_rich_dict() == sm.to_rich_dict()
-    sm = get_model("GNC")
-    data = sm.to_json()
-    got = deserialise_object(data)
-    assert got.to_rich_dict() == sm.to_rich_dict()
+    assert got.get_param_list() == sm.get_param_list()
+    assert isinstance(got, type(sm))
+
+
+def test_roundtrip_non_reversible_submod():
     sm = NonReversibleDinucleotide(_sym_preds)
     data = sm.to_json()
     got = deserialise_object(data)
-    assert got.to_rich_dict() == sm.to_rich_dict()
+    assert got.get_param_list() == sm.get_param_list()
+    assert isinstance(got, type(sm))
+
     sm = NonReversibleTrinucleotide(_sym_preds)
     data = sm.to_json()
     got = deserialise_object(data)
-    assert got.to_rich_dict() == sm.to_rich_dict()
+    assert got.get_param_list() == sm.get_param_list()
+    assert isinstance(got, type(sm))
 
 
-def test_roundtrip_discrete_time_submod():
+@pytest.mark.parametrize("motif_length", [1, 2])
+def test_roundtrip_discrete_time_submod(motif_length):
     """discrete time substitution models to_json enables roundtrip"""
-    sm = get_model("DT")
-    data = sm.to_json()
-    got = deserialise_object(data)
-    assert got.to_rich_dict() == sm.to_rich_dict()
-
-    sm = get_model("DT", motif_length=2)
-    data = sm.to_json()
+    sm = get_model("DT", motif_length=motif_length)
+    assert sm.word_length == motif_length
+    data = sm.to_rich_dict()
     got = deserialise_object(data)
-    assert got.to_rich_dict() == sm.to_rich_dict()
+    assert isinstance(got, type(sm))
+    assert got.get_param_list() == sm.get_param_list()
+    assert got.word_length == motif_length
 
 
 def test_roundtrip_likelihood_function():
@@ -572,70 +556,6 @@ def test_convert_annotation_to_annotation_db():
     assert db.num_matches() == 1
 
 
-def test_deser_annotated_aln():
-    data = {
-        "seqs": {
-            "A": {
-                "name": "A",
-                "seq": "--TTGTAGTTGA",
-                "moltype": "dna",
-                "info": None,
-                "type": "cogent3.core.sequence.DnaSequence",
-                "version": "2023.2.12a1",
-            },
-            "B": {
-                "name": "B",
-                "seq": "AATTGTAGTTGA",
-                "moltype": "dna",
-                "info": None,
-                "type": "cogent3.core.sequence.DnaSequence",
-                "version": "2023.2.12a1",
-            },
-        },
-        "moltype": "dna",
-        "info": {"source": "unknown"},
-        "type": "cogent3.core.alignment.Alignment",
-        "version": "2023.2.12a1",
-        "annotations": [
-            {
-                "annotation_construction": {
-                    "type": "CDS",
-                    "name": "norwegian",
-                    "map": {
-                        "spans": [
-                            {
-                                "start": 0,
-                                "end": 3,
-                                "value": None,
-                                "reverse": False,
-                                "type": "cogent3.core.location.Span",
-                                "version": "2023.2.12a1",
-                            },
-                            {
-                                "start": 5,
-                                "end": 9,
-                                "value": None,
-                                "reverse": False,
-                                "type": "cogent3.core.location.Span",
-                                "version": "2023.2.12a1",
-                            },
-                        ],
-                        "parent_length": 12,
-                        "type": "cogent3.core.location.FeatureMap",
-                        "version": "2023.2.12a1",
-                    },
-                },
-                "type": "cogent3.core.annotation.AnnotatableFeature",
-                "version": "2023.2.12a1",
-            },
-        ],
-    }
-    aln = deserialise_object(data)
-    assert aln.annotation_db.num_matches() == 1
-    feat = list(aln.get_features(biotype="CDS"))
-    assert len(feat) == 1
-
-
 @pytest.mark.parametrize("rate_matrix_required", [True, False])
 def test_roundtrip_TN93_model(rate_matrix_required):
     """model_result of split codon correct type after roundtrip"""
@@ -673,7 +593,7 @@ def test_roundtrip_TN93_model_result():
 @pytest.mark.parametrize("mtype", ["dna", "protein"])
 def test_roundtrip_seq(mtype):
     """seq to_json enables roundtrip"""
-    mtype = moltype.get_moltype(mtype)
+    mtype = get_moltype(mtype, new_type=True)
     seq = mtype.make_seq(seq="ACGGTCGG", name="label", info={"something": 3})
     got = deserialise_object(seq.to_json())
     assert got.info.something == 3
@@ -688,3 +608,680 @@ def test_dser_submodel(mn):
     rd = sm.to_rich_dict(for_pickle=False)
     got = deserialise_object(rd)
     assert got.moltype.alphabet.moltype is got.moltype
+
+
+def test_deserialise_old_to_new_type_alignment_1():
+    from cogent3.util.misc import get_object_provenance
+
+    rd = {
+        "seqs": {
+            "Human": {
+                "name": "Human",
+                "seq": {
+                    "type": "cogent3.core.sequence.SeqView",
+                    "version": "2025.5.8a9",
+                    "init_args": {
+                        "seq": "TGTGGCACAAATACTCATGC",
+                        "seqid": "Human",
+                        "step": 1,
+                    },
+                },
+                "moltype": "dna",
+                "info": None,
+                "type": "cogent3.core.sequence.DnaSequence",
+                "version": "2025.5.8a9",
+                "annotation_offset": 0,
+            },
+            "Wombat": {
+                "name": "Wombat",
+                "seq": {
+                    "type": "cogent3.core.sequence.SeqView",
+                    "version": "2025.5.8a9",
+                    "init_args": {
+                        "seq": "---------------NNTGC",
+                        "seqid": "Wombat",
+                        "step": 1,
+                    },
+                },
+                "moltype": "dna",
+                "info": None,
+                "type": "cogent3.core.sequence.DnaSequence",
+                "version": "2025.5.8a9",
+                "annotation_offset": 0,
+            },
+            "Mouse": {
+                "name": "Mouse",
+                "seq": {
+                    "type": "cogent3.core.sequence.SeqView",
+                    "version": "2025.5.8a9",
+                    "init_args": {
+                        "seq": "TGTGGCACAGATGCTCATGC",
+                        "seqid": "Mouse",
+                        "step": 1,
+                    },
+                },
+                "moltype": "dna",
+                "info": None,
+                "type": "cogent3.core.sequence.DnaSequence",
+                "version": "2025.5.8a9",
+                "annotation_offset": 0,
+            },
+        },
+        "moltype": "dna",
+        "info": {"source": "unknown"},
+        "type": "cogent3.core.alignment.ArrayAlignment",
+        "version": "2025.5.8a9",
+    }
+    got = deserialise_object(rd)
+    got_prov = get_object_provenance(got)
+    assert got_prov == "cogent3.core.new_alignment.Alignment"
+    rd = {
+        "info": {"seed": "758_443154_73021", "source": "15", "fg_edge": "73021"},
+        "moltype": "dna",
+        "seqs": {
+            "443154": {
+                "info": None,
+                "moltype": "dna",
+                "name": "443154",
+                "seq": "CAAGCATAATAA",
+                "type": "cogent3.core.sequence.DnaSequence",
+                "version": "2022.10.31a1",
+            },
+            "73021": {
+                "info": None,
+                "moltype": "dna",
+                "name": "73021",
+                "seq": "CAAGTATCCTAA",
+                "type": "cogent3.core.sequence.DnaSequence",
+                "version": "2022.10.31a1",
+            },
+            "758": {
+                "info": None,
+                "moltype": "dna",
+                "name": "758",
+                "seq": "TGAATATCATAA",
+                "type": "cogent3.core.sequence.DnaSequence",
+                "version": "2022.10.31a1",
+            },
+        },
+        "type": "cogent3.core.alignment.ArrayAlignment",
+        "version": "2022.10.31a1",
+    }
+    got = deserialise_object(rd)
+    got_prov = get_object_provenance(got)
+    assert got_prov == "cogent3.core.new_alignment.Alignment"
+    assert got.source == "15"
+    assert "source" not in got.info
+
+
+def test_deserialise_old_to_new_type_alignment_2():
+    from cogent3.util.misc import get_object_provenance
+
+    rd = {
+        "seqs": {
+            "Human": {
+                "version": "2025.5.8a9",
+                "type": "cogent3.core.alignment.Aligned",
+                "map_init": {
+                    "gap_pos": [],
+                    "cum_gap_lengths": [],
+                    "termini_unknown": False,
+                    "parent_length": 20,
+                    "type": "cogent3.core.location.IndelMap",
+                    "version": "2025.5.8a9",
+                },
+                "seq_init": {
+                    "name": "Human",
+                    "seq": {
+                        "type": "cogent3.core.sequence.SeqView",
+                        "version": "2025.5.8a9",
+                        "init_args": {
+                            "seq": "TGTGGCACAAATACTCATGC",
+                            "seqid": "Human",
+                            "step": 1,
+                        },
+                    },
+                    "moltype": "dna",
+                    "info": None,
+                    "type": "cogent3.core.sequence.DnaSequence",
+                    "version": "2025.5.8a9",
+                    "annotation_offset": 0,
+                },
+            },
+            "Wombat": {
+                "version": "2025.5.8a9",
+                "type": "cogent3.core.alignment.Aligned",
+                "map_init": {
+                    "gap_pos": [0],
+                    "cum_gap_lengths": [15],
+                    "termini_unknown": False,
+                    "parent_length": 5,
+                    "type": "cogent3.core.location.IndelMap",
+                    "version": "2025.5.8a9",
+                },
+                "seq_init": {
+                    "name": "Wombat",
+                    "seq": {
+                        "type": "cogent3.core.sequence.SeqView",
+                        "version": "2025.5.8a9",
+                        "init_args": {"seq": "NNTGC", "seqid": "Wombat", "step": 1},
+                    },
+                    "moltype": "dna",
+                    "info": None,
+                    "type": "cogent3.core.sequence.DnaSequence",
+                    "version": "2025.5.8a9",
+                    "annotation_offset": 0,
+                },
+            },
+            "Mouse": {
+                "version": "2025.5.8a9",
+                "type": "cogent3.core.alignment.Aligned",
+                "map_init": {
+                    "gap_pos": [],
+                    "cum_gap_lengths": [],
+                    "termini_unknown": False,
+                    "parent_length": 20,
+                    "type": "cogent3.core.location.IndelMap",
+                    "version": "2025.5.8a9",
+                },
+                "seq_init": {
+                    "name": "Mouse",
+                    "seq": {
+                        "type": "cogent3.core.sequence.SeqView",
+                        "version": "2025.5.8a9",
+                        "init_args": {
+                            "seq": "TGTGGCACAGATGCTCATGC",
+                            "seqid": "Mouse",
+                            "step": 1,
+                        },
+                    },
+                    "moltype": "dna",
+                    "info": None,
+                    "type": "cogent3.core.sequence.DnaSequence",
+                    "version": "2025.5.8a9",
+                    "annotation_offset": 0,
+                },
+            },
+        },
+        "moltype": "dna",
+        "info": {"source": "unknown"},
+        "type": "cogent3.core.alignment.Alignment",
+        "version": "2025.5.8a9",
+    }
+    got = deserialise_object(rd)
+    got_prov = get_object_provenance(got)
+    assert got_prov == "cogent3.core.new_alignment.Alignment"
+    assert got.source == "unknown"
+    assert "source" not in got.info
+
+
+def test_deserialise_old_to_new_type_seqcoll():
+    from cogent3.util.misc import get_object_provenance
+
+    rd = {
+        "seqs": {
+            "Human": {
+                "name": "Human",
+                "seq": {
+                    "type": "cogent3.core.sequence.SeqView",
+                    "version": "2025.5.8a9",
+                    "init_args": {
+                        "seq": "TGTGGCACAAATACTCATGC",
+                        "seqid": "Human",
+                        "step": 1,
+                    },
+                },
+                "moltype": "dna",
+                "info": None,
+                "type": "cogent3.core.sequence.DnaSequence",
+                "version": "2025.5.8a9",
+                "annotation_offset": 0,
+            },
+            "Wombat": {
+                "name": "Wombat",
+                "seq": {
+                    "type": "cogent3.core.sequence.SeqView",
+                    "version": "2025.5.8a9",
+                    "init_args": {
+                        "seq": "---------------NNTGC",
+                        "seqid": "Wombat",
+                        "step": 1,
+                    },
+                },
+                "moltype": "dna",
+                "info": None,
+                "type": "cogent3.core.sequence.DnaSequence",
+                "version": "2025.5.8a9",
+                "annotation_offset": 0,
+            },
+            "Mouse": {
+                "name": "Mouse",
+                "seq": {
+                    "type": "cogent3.core.sequence.SeqView",
+                    "version": "2025.5.8a9",
+                    "init_args": {
+                        "seq": "TGTGGCACAGATGCTCATGC",
+                        "seqid": "Mouse",
+                        "step": 1,
+                    },
+                },
+                "moltype": "dna",
+                "info": None,
+                "type": "cogent3.core.sequence.DnaSequence",
+                "version": "2025.5.8a9",
+                "annotation_offset": 0,
+            },
+        },
+        "moltype": "dna",
+        "info": {"source": "unknown"},
+        "type": "cogent3.core.alignment.SequenceCollection",
+        "version": "2025.5.8a9",
+    }
+    got = deserialise_object(rd)
+    got_prov = get_object_provenance(got)
+    assert got_prov == "cogent3.core.new_alignment.SequenceCollection"
+    assert got.source == "unknown"
+    assert "source" not in got.info
+
+
+moltype_old_class_map = {
+    "dna": "cogent3.core.sequence.DnaSequence",
+    "rna": "cogent3.core.sequence.RnaSequence",
+    "text": "cogent3.core.sequence.Sequence",
+    "bytes": "cogent3.core.sequence.ByteSequence",
+    "protein": "cogent3.core.sequence.ProteinSequence",
+    "protein_with_stop": "cogent3.core.sequence.ProteinWithStopSequence",
+}
+
+
+@pytest.mark.parametrize("moltype_name", list(moltype_old_class_map.keys()))
+def test_deserialise_old_to_new_type_seqs(moltype_name):
+    from cogent3.core import new_moltype, new_sequence
+
+    old_rich_dict = {
+        "annotation_offset": 0,
+        "info": {"something": 3},
+        "moltype": moltype_name,
+        "name": "label",
+        "seq": {
+            "init_args": {"seq": "ACGGTCGG", "seqid": "label", "step": 1},
+            "type": "cogent3.core.sequence.SeqView",
+            "version": "2025.5.8a9",
+        },
+        "type": moltype_old_class_map[moltype_name],
+        "version": "2025.5.8a9",
+    }
+    got = deserialise_object(old_rich_dict)
+    assert isinstance(got.moltype, new_moltype.MolType)
+    assert isinstance(got, new_sequence.Sequence)
+
+
+def test_deserialise_old_to_new_type_moltype():
+    from cogent3.core import new_moltype
+
+    old_rich_dict = {
+        "type": "cogent3.core.moltype.MolType",
+        "moltype": "dna",
+        "version": "2025.5.8a9",
+    }
+    got = deserialise_object(old_rich_dict)
+    assert isinstance(got, new_moltype.MolType)
+
+
+@pytest.mark.parametrize(
+    "motifset", ["TCAG", "TCAGNRYWSKMBDHV", "TCAG-", "TCAG-NRYWSKMBDHV?"]
+)
+def test_deserialise_old_to_new_charalphabet(motifset):
+    from cogent3.core import new_alphabet
+
+    old_rich_dict = {
+        "motifset": list(motifset),
+        "gap": "-",
+        "moltype": "dna",
+        "type": "cogent3.core.alphabet.CharAlphabet",
+        "version": "2025.5.8a9",
+    }
+    got = deserialise_object(old_rich_dict)
+    assert isinstance(got, new_alphabet.CharAlphabet)
+
+
+def test_deserialise_old_to_new_alphabet():
+    # this is special to BH
+    from cogent3.core import new_alphabet
+
+    old_rich_dict = {
+        "motifset": ("T", "C", "A", "G"),
+        "gap": "-",
+        "moltype": "dna",
+        "type": "cogent3.core.alphabet.Alphabet",
+        "version": "2025.5.8a9",
+    }
+    got = deserialise_object(old_rich_dict)
+    assert isinstance(got, new_alphabet.CharAlphabet)
+
+
+def test_deserialise_old_to_new_type_kmer_alphabet():
+    from cogent3.core import new_alphabet
+
+    old_rich_dict = {
+        "data": ["TCAG", "TCAG"],
+        "gap": None,
+        "moltype": "dna",
+        "type": "cogent3.core.alphabet.JointEnumeration",
+        "version": "2025.5.8a9",
+    }
+    got = deserialise_object(old_rich_dict)
+    assert isinstance(got, new_alphabet.KmerAlphabet)
+    assert len(got) == 16
+
+
+def test_deserialise_old_to_new_type_codon_alphabet():
+    from cogent3.core import new_alphabet
+
+    old_rich_dict = {
+        "motifset": (
+            "TTT",
+            "TTC",
+            "TTA",
+            "TTG",
+            "TCT",
+            "TCC",
+            "TCA",
+            "TCG",
+            "TAT",
+            "TAC",
+            "TGT",
+            "TGC",
+            "TGG",
+            "CTT",
+            "CTC",
+            "CTA",
+            "CTG",
+            "CCT",
+            "CCC",
+            "CCA",
+            "CCG",
+            "CAT",
+            "CAC",
+            "CAA",
+            "CAG",
+            "CGT",
+            "CGC",
+            "CGA",
+            "CGG",
+            "ATT",
+            "ATC",
+            "ATA",
+            "ATG",
+            "ACT",
+            "ACC",
+            "ACA",
+            "ACG",
+            "AAT",
+            "AAC",
+            "AAA",
+            "AAG",
+            "AGT",
+            "AGC",
+            "AGA",
+            "AGG",
+            "GTT",
+            "GTC",
+            "GTA",
+            "GTG",
+            "GCT",
+            "GCC",
+            "GCA",
+            "GCG",
+            "GAT",
+            "GAC",
+            "GAA",
+            "GAG",
+            "GGT",
+            "GGC",
+            "GGA",
+            "GGG",
+        ),
+        "gap": "-",
+        "moltype": "dna",
+        "type": "cogent3.core.alphabet.Alphabet",
+        "version": "2025.5.8a9",
+    }
+    alpha = deserialise_object(old_rich_dict)
+    assert isinstance(alpha, new_alphabet.SenseCodonAlphabet)
+
+
+def test_deserialise_old_to_new_type_submodel():
+    from cogent3.evolve.ns_substitution_model import DiscreteSubstitutionModel
+    from cogent3.evolve.substitution_model import TimeReversibleNucleotide
+
+    old_rich_dict = {
+        "alphabet": {
+            "motifset": ("T", "C", "A", "G"),
+            "gap": "-",
+            "moltype": "dna",
+            "type": "cogent3.core.alphabet.Alphabet",
+            "version": "2025.5.8a9",
+        },
+        "motif_probs": None,
+        "optimise_motif_probs": True,
+        "equal_motif_probs": False,
+        "motif_probs_from_data": None,
+        "motif_probs_alignment": None,
+        "mprob_model": "tuple",
+        "model_gaps": False,
+        "recode_gaps": False,
+        "motif_length": 1,
+        "name": "BH",
+        "motifs": None,
+        "type": "cogent3.evolve.ns_substitution_model.DiscreteSubstitutionModel",
+        "version": "2025.5.8a9",
+    }
+    got = deserialise_object(old_rich_dict)
+    assert isinstance(got, DiscreteSubstitutionModel)
+
+    old_rich_dict = {
+        "alphabet": {
+            "motifset": ("T", "C", "A", "G"),
+            "gap": "-",
+            "moltype": "dna",
+            "type": "cogent3.core.alphabet.CharAlphabet",
+            "version": "2025.5.8a9",
+        },
+        "motif_probs": None,
+        "optimise_motif_probs": False,
+        "equal_motif_probs": False,
+        "motif_probs_from_data": None,
+        "motif_probs_alignment": None,
+        "mprob_model": None,
+        "model_gaps": False,
+        "recode_gaps": True,
+        "motif_length": 1,
+        "name": "F81",
+        "motifs": None,
+        "with_rate": False,
+        "ordered_param": None,
+        "distribution": None,
+        "partitioned_params": None,
+        "kw": {},
+        "predicates": [],
+        "scales": None,
+        "type": "cogent3.evolve.substitution_model.TimeReversibleNucleotide",
+        "version": "2025.5.8a9",
+    }
+
+    got = deserialise_object(old_rich_dict)
+    assert isinstance(got, TimeReversibleNucleotide)
+
+
+def test_deserialise_old_to_new_type_likelihood():
+    old_json = {
+        "model": {
+            "alphabet": {
+                "motifset": ["T", "C", "A", "G"],
+                "gap": "-",
+                "moltype": "dna",
+                "type": "cogent3.core.alphabet.Alphabet",
+                "version": "2025.5.8a9",
+            },
+            "motif_probs": None,
+            "optimise_motif_probs": True,
+            "equal_motif_probs": False,
+            "motif_probs_from_data": None,
+            "motif_probs_alignment": None,
+            "mprob_model": "tuple",
+            "model_gaps": False,
+            "recode_gaps": False,
+            "motif_length": 1,
+            "name": "BH",
+            "motifs": None,
+            "type": "cogent3.evolve.ns_substitution_model.DiscreteSubstitutionModel",
+            "version": "2025.5.8a9",
+        },
+        "tree": {
+            "newick": "(Human,Mouse,Opossum)root",
+            "edge_attributes": {
+                "Human": {"length": None},
+                "Mouse": {"length": None},
+                "Opossum": {"length": None},
+                "root": {"length": None},
+            },
+            "type": "cogent3.core.tree.PhyloNode",
+            "version": "2025.5.8a9",
+        },
+        "alignment": {
+            "seqs": {
+                "Human": {
+                    "name": "Human",
+                    "seq": {
+                        "type": "cogent3.core.sequence.SeqView",
+                        "version": "2025.5.8a9",
+                        "init_args": {
+                            "seq": "ATGCGGCTCGCGGAGGCCGCGCTCGCGGAG",
+                            "seqid": "Human",
+                            "step": 1,
+                        },
+                    },
+                    "moltype": "dna",
+                    "info": None,
+                    "type": "cogent3.core.sequence.DnaSequence",
+                    "version": "2025.5.8a9",
+                    "annotation_offset": 0,
+                },
+                "Mouse": {
+                    "name": "Mouse",
+                    "seq": {
+                        "type": "cogent3.core.sequence.SeqView",
+                        "version": "2025.5.8a9",
+                        "init_args": {
+                            "seq": "ATGCCCGGCGCCAAGGCAGCGCTGGCGGAG",
+                            "seqid": "Mouse",
+                            "step": 1,
+                        },
+                    },
+                    "moltype": "dna",
+                    "info": None,
+                    "type": "cogent3.core.sequence.DnaSequence",
+                    "version": "2025.5.8a9",
+                    "annotation_offset": 0,
+                },
+                "Opossum": {
+                    "name": "Opossum",
+                    "seq": {
+                        "type": "cogent3.core.sequence.SeqView",
+                        "version": "2025.5.8a9",
+                        "init_args": {
+                            "seq": "ATGCCAGTGAAAGTGGCGGCGGTGGCTGAG",
+                            "seqid": "Opossum",
+                            "step": 1,
+                        },
+                    },
+                    "moltype": "dna",
+                    "info": None,
+                    "type": "cogent3.core.sequence.DnaSequence",
+                    "version": "2025.5.8a9",
+                    "annotation_offset": 0,
+                },
+            },
+            "moltype": "dna",
+            "info": {"source": "unknown"},
+            "type": "cogent3.core.alignment.ArrayAlignment",
+            "version": "2025.5.8a9",
+        },
+        "likelihood_construction": {
+            "bins": 1,
+            "loci": 1,
+            "optimise_motif_probs": True,
+            "motif_probs_from_align": True,
+            "default_length": 1.0,
+        },
+        "param_rules": [
+            {
+                "par_name": "psubs",
+                "edge": "Human",
+                "init": {
+                    "T": {
+                        "T": 0.9971099722856885,
+                        "C": 0.002886010264321592,
+                        "A": 2.008724995022132e-06,
+                        "G": 2.008724995022132e-06,
+                    },
+                    "C": {"T": 0.125, "C": 0.625, "A": 0.125, "G": 0.125},
+                    "A": {"T": 0.125, "C": 0.125, "A": 0.625, "G": 0.12499999999999997},
+                    "G": {"T": 0.125, "C": 0.125, "A": 0.125, "G": 0.625},
+                },
+                "lower": None,
+                "upper": None,
+            },
+            {
+                "par_name": "psubs",
+                "edge": "Mouse",
+                "init": {
+                    "T": {"T": 0.625, "C": 0.12499999999999997, "A": 0.125, "G": 0.125},
+                    "C": {"T": 0.125, "C": 0.625, "A": 0.125, "G": 0.125},
+                    "A": {"T": 0.125, "C": 0.125, "A": 0.625, "G": 0.12499999999999997},
+                    "G": {"T": 0.125, "C": 0.125, "A": 0.125, "G": 0.625},
+                },
+                "lower": None,
+                "upper": None,
+            },
+            {
+                "par_name": "psubs",
+                "edge": "Opossum",
+                "init": {
+                    "T": {"T": 0.625, "C": 0.12499999999999997, "A": 0.125, "G": 0.125},
+                    "C": {"T": 0.125, "C": 0.625, "A": 0.125, "G": 0.125},
+                    "A": {"T": 0.125, "C": 0.125, "A": 0.625, "G": 0.12499999999999997},
+                    "G": {"T": 0.125, "C": 0.125, "A": 0.125, "G": 0.625},
+                },
+                "lower": None,
+                "upper": None,
+            },
+            {
+                "par_name": "mprobs",
+                "init": {
+                    "T": 0.11111111111111109,
+                    "C": 0.27777777777777773,
+                    "A": 0.15555555555555559,
+                    "G": 0.45555555555555555,
+                },
+                "lower": None,
+                "upper": None,
+            },
+        ],
+        "lnL": -98.78628627311876,
+        "nfp": 39,
+        "motif_probs": {
+            "T": 0.11111111111111109,
+            "C": 0.27777777777777773,
+            "A": 0.15555555555555559,
+            "G": 0.45555555555555555,
+        },
+        "DLC": True,
+        "unique_Q": None,
+        "type": "cogent3.evolve.parameter_controller.AlignmentLikelihoodFunction",
+        "name": "BH",
+        "version": "2025.5.8a9",
+    }
+    lf = deserialise_likelihood_function(old_json)
+    assert pytest.approx(lf.lnL) == -98.78628627311876