diff --git a/astropy/cosmology/__init__.py b/astropy/cosmology/__init__.py
index 4fc4d01ce501..a9c581e8b501 100644
--- a/astropy/cosmology/__init__.py
+++ b/astropy/cosmology/__init__.py
@@ -8,7 +8,9 @@

 """

-from . import core, funcs, realizations, units, utils
+from . import core, funcs, units, utils
+from . import io  # isort: split  # needed before 'realizations'
+from . import realizations
 from .core import *
 from .funcs import *
 from .realizations import *
diff --git a/astropy/cosmology/connect.py b/astropy/cosmology/connect.py
index bfe84f2e836c..f1161eb1859d 100644
--- a/astropy/cosmology/connect.py
+++ b/astropy/cosmology/connect.py
@@ -6,10 +6,14 @@
 from astropy.io import registry as io_registry
 from astropy.utils.exceptions import AstropyUserWarning

-__all__ = ["CosmologyRead", "CosmologyWrite"]
+__all__ = ["CosmologyRead", "CosmologyWrite",
+           "CosmologyFromFormat", "CosmologyToFormat"]
 __doctest_skip__ = __all__

+# ==============================================================================
+# Read / Write
+
 class CosmologyRead(io_registry.UnifiedReadWrite):
     """Read and parse data to a `~astropy.cosmology.Cosmology`.

@@ -18,10 +22,10 @@ class CosmologyRead(io_registry.UnifiedReadWrite):
     syntax such as::

         >>> from astropy.cosmology import Cosmology
-        >>> cosmo1 = Cosmology.read('[file name]')
+        >>> cosmo1 = Cosmology.read('<file name>')

     When the ``read`` method is called from a subclass the subclass will
-    provide a keyword argument ``cosmology=[class]`` to the registered read
+    provide a keyword argument ``cosmology=<class>`` to the registered read
     method. The method uses this cosmology class, regardless of the class
     indicated in the file, and sets parameters' default values from the class'
     signature.
@@ -29,11 +33,19 @@ class CosmologyRead(io_registry.UnifiedReadWrite):
     Get help on the available readers using the ``help()`` method::

       >>> Cosmology.read.help()  # Get help reading and list supported formats
-      >>> Cosmology.read.help('[format]')  # Get detailed help on a format
+      >>> Cosmology.read.help(format='<format>')  # Get detailed help on a format
       >>> Cosmology.read.list_formats()  # Print list of available formats

     See also: https://docs.astropy.org/en/stable/io/unified.html

+    .. note::
+
+        :meth:`~astropy.cosmology.Cosmology.read` and
+        :meth:`~astropy.cosmology.Cosmology.from_format` currently access the
+        same registry. This will be deprecated and formats intended for
+        ``from_format`` should not be used here. Use ``Cosmology.read.help()``
+        to confirm that the format may be used to read a file.
+
     Parameters
     ----------
     *args
@@ -48,6 +60,9 @@ class CosmologyRead(io_registry.UnifiedReadWrite):
     -------
     out : `~astropy.cosmology.Cosmology` subclass instance
         `~astropy.cosmology.Cosmology` corresponding to file contents.
+
+    Notes
+    -----
     """

     def __init__(self, instance, cosmo_cls):
@@ -77,19 +92,27 @@ class CosmologyWrite(io_registry.UnifiedReadWrite):
     """Write this Cosmology object out in the specified format.

     This function provides the Cosmology interface to the astropy unified I/O
-    layer.  This allows easily writing a file in supported data formats
+    layer. This allows easily writing a file in supported data formats
     using syntax such as::

       >>> from astropy.cosmology import Planck18
-      >>> Planck18.write('[file name]')
+      >>> Planck18.write('<file name>')

-    Get help on the available writers for ``Cosmology`` using the``help()``
+    Get help on the available writers for ``Cosmology`` using the ``help()``
     method::

       >>> Cosmology.write.help()  # Get help writing and list supported formats
-      >>> Cosmology.write.help('[format]')  # Get detailed help on format
+      >>> Cosmology.write.help(format='<format>')  # Get detailed help on format
       >>> Cosmology.write.list_formats()  # Print list of available formats

+    .. note::
+
+        :meth:`~astropy.cosmology.Cosmology.write` and
+        :meth:`~astropy.cosmology.Cosmology.to_format` currently access the
+        same registry. This will be deprecated and formats intended for
+        ``to_format`` should not be used here. Use ``Cosmology.write.help()``
+        to confirm that the format may be used to write to a file.
+
     Parameters
     ----------
     *args
@@ -99,6 +122,9 @@ class CosmologyWrite(io_registry.UnifiedReadWrite):
         File format specifier.
     **kwargs
         Keyword arguments passed through to data writer.
+
+    Notes
+    -----
     """

     def __init__(self, instance, cls):
@@ -106,3 +132,129 @@ def __init__(self, instance, cls):

     def __call__(self, *args, **kwargs):
         io_registry.write(self._instance, *args, **kwargs)
+
+
+# ==============================================================================
+# Format Interchange
+# for transforming instances, e.g. Cosmology <-> dict
+
+class CosmologyFromFormat(io_registry.UnifiedReadWrite):
+    """Transform object to a `~astropy.cosmology.Cosmology`.
+
+    This function provides the Cosmology interface to the Astropy unified I/O
+    layer. This allows easily parsing supported data formats using
+    syntax such as::
+
+      >>> from astropy.cosmology import Cosmology
+      >>> cosmo1 = Cosmology.from_format(cosmo_mapping, format='mapping')
+
+    When the ``from_format`` method is called from a subclass the subclass will
+    provide a keyword argument ``cosmology=<class>`` to the registered parser.
+    The method uses this cosmology class, regardless of the class indicated in
+    the data, and sets parameters' default values from the class' signature.
+
+    Get help on the available readers using the ``help()`` method::
+
+      >>> Cosmology.from_format.help()  # Get help and list supported formats
+      >>> Cosmology.from_format.help('<format>')  # Get detailed help on a format
+      >>> Cosmology.from_format.list_formats()  # Print list of available formats
+
+    See also: https://docs.astropy.org/en/stable/io/unified.html
+
+    .. note::
+
+        :meth:`~astropy.cosmology.Cosmology.from_format` and
+        :meth:`~astropy.cosmology.Cosmology.read` currently access the
+        same registry. This will be deprecated and formats intended for
+        ``read`` should not be used here. Use ``Cosmology.to_format.help()``
+        to confirm that the format may be used to convert to a Cosmology.
+
+    Parameters
+    ----------
+    obj : object
+        The object to parse according to 'format'
+    *args
+        Positional arguments passed through to data parser.
+    format : str (optional, keyword-only)
+        Object format specifier.
+    **kwargs
+        Keyword arguments passed through to data parser.
+
+    Returns
+    -------
+    out : `~astropy.cosmology.Cosmology` subclass instance
+        `~astropy.cosmology.Cosmology` corresponding to ``obj`` contents.
+
+    """
+
+    def __init__(self, instance, cosmo_cls):
+        super().__init__(instance, cosmo_cls, "read")
+
+    def __call__(self, obj, *args, **kwargs):
+        from astropy.cosmology.core import Cosmology
+
+        # so subclasses can override, also pass the class as a kwarg.
+        # allows for `FlatLambdaCDM.read` and
+        # `Cosmology.read(..., cosmology=FlatLambdaCDM)`
+        if self._cls is not Cosmology:
+            kwargs.setdefault("cosmology", self._cls)  # set, if not present
+            # check that it is the correct cosmology, can be wrong if user
+            # passes in e.g. `w0wzCDM.read(..., cosmology=FlatLambdaCDM)`
+            valid = (self._cls, self._cls.__qualname__)
+            if kwargs["cosmology"] not in valid:
+                raise ValueError(
+                    "keyword argument `cosmology` must be either the class "
+                    f"{valid[0]} or its qualified name '{valid[1]}'")
+
+        cosmo = io_registry.read(self._cls, obj, *args, **kwargs)
+        return cosmo
+
+
+class CosmologyToFormat(io_registry.UnifiedReadWrite):
+    """Transform this Cosmology to another format.
+
+    This function provides the Cosmology interface to the astropy unified I/O
+    layer. This allows easily transforming to supported data formats
+    using syntax such as::
+
+      >>> from astropy.cosmology import Planck18
+      >>> Planck18.to_format("mapping")
+      {'cosmology': astropy.cosmology.core.FlatLambdaCDM,
+       'name': 'Planck18',
+       'H0': <Quantity 67.66 km / (Mpc s)>,
+       'Om0': 0.30966,
+       ...
+
+    Get help on the available representations for ``Cosmology`` using the
+    ``help()`` method::
+
+      >>> Cosmology.to_format.help()  # Get help and list supported formats
+      >>> Cosmology.to_format.help('<format>')  # Get detailed help on format
+      >>> Cosmology.to_format.list_formats()  # Print list of available formats
+
+    .. note::
+
+        :meth:`~astropy.cosmology.Cosmology.to_format` and
+        :meth:`~astropy.cosmology.Cosmology.write` currently access the
+        same registry. This will be deprecated and formats intended for
+        ``write`` should not be used here. Use ``Cosmology.to_format.help()``
+        to confirm that the format may be used to convert a Cosmology.
+
+    Parameters
+    ----------
+    format : str
+        Format specifier.
+    *args
+        Positional arguments passed through to data writer. If supplied the
+        first argument is the output filename.
+    **kwargs
+        Keyword arguments passed through to data writer.
+
+    """
+
+    def __init__(self, instance, cls):
+        super().__init__(instance, cls, "write")
+
+    def __call__(self, format, *args, **kwargs):
+        return io_registry.write(self._instance, None, *args, format=format,
+                                 **kwargs)
diff --git a/astropy/cosmology/core.py b/astropy/cosmology/core.py
index f5fbf4d96aad..dfd48f354349 100644
--- a/astropy/cosmology/core.py
+++ b/astropy/cosmology/core.py
@@ -18,7 +18,7 @@
 from astropy.utils.metadata import MetaData

 from . import scalar_inv_efuncs
-from .connect import CosmologyRead, CosmologyWrite
+from .connect import CosmologyFromFormat, CosmologyRead, CosmologyToFormat, CosmologyWrite
 from .utils import _float_or_none, inf_like, vectorize_if_needed

 # Originally authored by Andrew Becker (becker@astro.washington.edu),
@@ -82,6 +82,10 @@ class Cosmology(metaclass=ABCMeta):

     meta = MetaData()

+    # Unified I/O object interchange methods
+    from_format = UnifiedReadWriteMethod(CosmologyFromFormat)
+    to_format = UnifiedReadWriteMethod(CosmologyToFormat)
+
     # Unified I/O read and write methods
     read = UnifiedReadWriteMethod(CosmologyRead)
     write = UnifiedReadWriteMethod(CosmologyWrite)
diff --git a/astropy/cosmology/io/__init__.py b/astropy/cosmology/io/__init__.py
new file mode 100644
index 000000000000..c7816e15805b
--- /dev/null
+++ b/astropy/cosmology/io/__init__.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# Licensed under a 3-clause BSD style license - see LICENSE.rst
+
+"""
+Read/Write/Interchange methods for `astropy.cosmology`. **NOT public API**.
+"""
+
+# Import the interchange to register them into the io registry.
+from . import mapping  # noqa: F403
diff --git a/astropy/cosmology/io/mapping.py b/astropy/cosmology/io/mapping.py
new file mode 100644
index 000000000000..05466305db75
--- /dev/null
+++ b/astropy/cosmology/io/mapping.py
@@ -0,0 +1,183 @@
+# Licensed under a 3-clause BSD style license - see LICENSE.rst
+
+"""
+The following are private functions, included here **FOR REFERENCE ONLY** since
+the io registry cannot be displayed. These functions are registered into
+:meth:`~astropy.cosmology.Cosmology.to_format` and
+:meth:`~astropy.cosmology.Cosmology.from_format` and should only be accessed
+via these methods.
+"""  # this is shown in the docs.
+
+import copy
+from collections.abc import Mapping
+
+import numpy as np
+
+from astropy.cosmology.core import _COSMOLOGY_CLASSES, Cosmology
+from astropy.io import registry as io_registry
+from astropy.table import QTable
+
+__all__ = ["from_mapping", "to_mapping"]
+
+
+def from_mapping(map, *, move_to_meta=False, cosmology=None):
+    """Load `~astropy.cosmology.Cosmology` from mapping object.
+
+    Parameters
+    ----------
+    map : mapping
+        Arguments into the class -- like "name" or "meta".
+        If 'cosmology' is None, must have field "cosmology" which can be either
+        the string name of the cosmology class (e.g. "FlatLambdaCDM") or the
+        class itself.
+
+    move_to_meta : bool (optional, keyword-only)
+        Whether to move keyword arguments that are not in the Cosmology class'
+        signature to the Cosmology's metadata. This will only be applied if the
+        Cosmology does NOT have a keyword-only argument (e.g. ``**kwargs``).
+        Arguments moved to the metadata will be merged with existing metadata,
+        preferring specified metadata in the case of a merge conflict
+        (e.g. for ``Cosmology(meta={'key':10}, key=42)``, the ``Cosmology.meta``
+        will be ``{'key': 10}``).
+
+    cosmology : str, `~astropy.cosmology.Cosmology` class, or None (optional, keyword-only)
+        The cosmology class (or string name thereof) to use when constructing
+        the cosmology instance. The class also provides default parameter values,
+        filling in any non-mandatory arguments missing in 'map'.
+
+    Returns
+    -------
+    `~astropy.cosmology.Cosmology` subclass instance
+
+    Examples
+    --------
+    To see loading a `~astropy.cosmology.Cosmology` from a dictionary with
+    ``from_mapping``, we will first make a mapping using
+    :meth:`~astropy.cosmology.Cosmology.to_format`.
+
+        >>> from astropy.cosmology import Cosmology, Planck18
+        >>> cm = Planck18.to_format('mapping')
+        >>> cm
+        {'cosmology': <class 'astropy.cosmology.core.FlatLambdaCDM'>,
+         'name': 'Planck18', 'H0': <Quantity 67.66 km / (Mpc s)>, 'Om0': 0.30966,
+         'Tcmb0': 2.7255, 'Neff': 3.046, 'm_nu': <Quantity [0. , 0. , 0.06] eV>,
+         'Ob0': 0.04897, 'meta': ...
+
+    Now this dict can be used to load a new cosmological instance identical
+    to the ``Planck18`` cosmology from which it was generated.
+
+        >>> cosmo = Cosmology.from_format(cm, format="mapping")
+        >>> cosmo
+        FlatLambdaCDM(name="Planck18", H0=67.7 km / (Mpc s), Om0=0.31,
+                      Tcmb0=2.725 K, Neff=3.05, m_nu=[0. 0. 0.06] eV, Ob0=0.049)
+
+    Specific cosmology classes can be used to parse the data. The class'
+    default parameter values are used to fill in any information missing in the
+    data.
+
+        >>> from astropy.cosmology import FlatLambdaCDM
+        >>> del cm["Tcmb0"]  # show FlatLambdaCDM provides default
+        >>> FlatLambdaCDM.from_format(cm)
+        FlatLambdaCDM(name="Planck18", H0=67.7 km / (Mpc s), Om0=0.31,
+                      Tcmb0=0 K, Neff=3.05, m_nu=None, Ob0=0.049)
+
+    """
+    params = copy.deepcopy(map)  # so can pop
+
+    # get cosmology
+    # 1st from 'kwargs'. Allows for override of the cosmology, if on file.
+    # 2nd from params. This MUST have the cosmology if 'kwargs' did not.
+    if cosmology is None:
+        cosmology = params.pop("cosmology")
+    else:
+        params.pop("cosmology", None)  # pop, but don't use
+    # if string, parse to class
+    if isinstance(cosmology, str):
+        cosmology = _COSMOLOGY_CLASSES[cosmology]
+
+    # select arguments from mapping that are in the cosmo's signature.
+    ba = cosmology._init_signature.bind_partial()  # blank set of args
+    ba.apply_defaults()  # fill in the defaults
+    for k in cosmology._init_signature.parameters.keys():
+        if k in params:  # transfer argument, if in params
+            ba.arguments[k] = params.pop(k)
+
+    # deal with remaining params. If there is a **kwargs use that, else
+    # allow to transfer to metadata. Raise TypeError if can't.
+    lastp = tuple(cosmology._init_signature.parameters.values())[-1]
+    if lastp.kind == 4:  # variable keyword-only
+        ba.arguments[lastp.name] = params
+    elif move_to_meta:  # prefers current meta, which was explicitly set
+        meta = ba.arguments["meta"] or {}  # (None -> dict)
+        ba.arguments["meta"] = {**params, **meta}
+    elif params:
+        raise TypeError(f"there are unused parameters {params}.")
+    # else: pass  # no kwargs, no move-to-meta, and all the params are used
+
+    return cosmology(*ba.args, **ba.kwargs)
+
+
+def to_mapping(cosmology, *args):
+    """Return the cosmology class, inputs, and metadata as a `dict`.
+
+    Parameters
+    ----------
+    cosmology : `~astropy.cosmology.Cosmology` subclass instance
+    *args
+        Not used. Needed for compatibility with
+        `~astropy.io.registry.UnifiedReadWriteMethod`
+
+    Returns
+    -------
+    dict
+        with key-values for the cosmology parameters and also:
+        - 'cosmology' : the class
+        - 'meta' : the contents of the cosmology's metadata attribute
+
+    Examples
+    --------
+    A Cosmology as a mapping will have the cosmology's name and
+    parameters as items, and the metadata as a nested dictionary.
+
+        >>> from astropy.cosmology import Planck18
+        >>> Planck18.to_format('mapping')
+        {'cosmology': <class 'astropy.cosmology.core.FlatLambdaCDM'>,
+         'name': 'Planck18', 'H0': <Quantity 67.66 km / (Mpc s)>, 'Om0': 0.30966,
+         'Tcmb0': 2.7255, 'Neff': 3.046, 'm_nu': <Quantity [0.  , 0.  , 0.06] eV>,
+         'Ob0': 0.04897, 'meta': ...
+
+    """
+
+    m = {}
+    # start with the cosmology class & name
+    m["cosmology"] = cosmology.__class__
+    m["name"] = cosmology.name  # here only for dict ordering
+    # get all the immutable inputs
+    m.update({k: v for k, v in cosmology._init_arguments.items()
+              if k not in ("meta", "name")})
+    # add the mutable metadata
+    m["meta"] = copy.deepcopy(cosmology.meta)
+
+    return m
+
+
+def mapping_identify(origin, format, *args, **kwargs):
+    """Identify if object uses the mapping format.
+
+    Returns
+    -------
+    bool
+    """
+    itis = False
+    if origin == "read":
+        itis = isinstance(args[1], Mapping) and (format in (None, "mapping"))
+
+    return itis
+
+
+# ===================================================================
+# Register
+
+io_registry.register_reader("mapping", Cosmology, from_mapping)
+io_registry.register_writer("mapping", Cosmology, to_mapping)
+io_registry.register_identifier("mapping", Cosmology, mapping_identify)
diff --git a/docs/changes/cosmology/11998.feature.rst b/docs/changes/cosmology/11998.feature.rst
new file mode 100644
index 000000000000..df78f84a93c9
--- /dev/null
+++ b/docs/changes/cosmology/11998.feature.rst
@@ -0,0 +1,5 @@
+Added ``to_format/from_format`` methods to Cosmology using the Unified I/O
+registry. Now custom format converters and format-identifier functions
+can be registered to transform Cosmology objects.
+The transformation between Cosmology and dictionaries is pre-registered.
+Details are discussed in an addition to the docs.
diff --git a/docs/cosmology/io.rst b/docs/cosmology/io.rst
index bb9f1881b4d1..db5f9509a287 100644
--- a/docs/cosmology/io.rst
+++ b/docs/cosmology/io.rst
@@ -1,25 +1,22 @@
-.. doctest-skip-all
-
 .. _read_write_cosmologies:

-Reading and Writing Cosmology Objects
-*************************************
+Read, Write, and Convert Cosmology Objects
+******************************************

 An easy way to serialize and deserialize a Cosmology object is using the
 :mod:`pickle` module.

-.. code-block:: python
-   :emphasize-lines: 1,4,8
+.. doctest-skip::

    >>> import pickle
    >>> from astropy.cosmology import Planck18
    >>> with open("planck18.pkl", mode="wb") as file:
-   ...     pickle.dump(file, Planck18)
-
-   And to read the file back
+   ...     pickle.dump(Planck18, file)
+   >>> # and to read back
    >>> with open("planck18.pkl", mode="rb") as file:
-   >>>     cosmo = pickle.load(file)
+   ...     cosmo = pickle.load(file)
    >>> cosmo
+   FlatLambdaCDM(name="Planck18", ...

 However this method has all the attendant drawbacks of :mod:`pickle` — security
 vulnerabilities and non-human-readable files.
@@ -43,34 +40,197 @@ framework.
 Writing a cosmology instance requires only the file location and optionally,
 if the file format cannot be inferred, a keyword argument "format". Additional
 positional arguments and keyword arguments are passed to the reader methods.
-::
+
+.. doctest-skip::

     >>> from astropy.cosmology import Planck18
-    >>> Planck18.write('[file name]')
+    >>> Planck18.write('<file name>')

 Reading back the cosmology is most safely done from ``Cosmology``, the base
 class, as it provides no default information and therefore requires the file
 to have all necessary information to describe a cosmology.

-::
+.. doctest-skip::

     >>> from astropy.cosmology import Cosmology
-    >>> cosmo = Cosmology.read('[file name]')
+    >>> cosmo = Cosmology.read('<file name>')
     >>> cosmo == Planck18
     True

 When a subclass of ``Cosmology`` is used to read a file, the subclass will
-provide a keyword argument ``cosmology=[class]`` to the registered read
+provide a keyword argument ``cosmology=<class>`` to the registered read
 method. The method uses this cosmology class, regardless of the class
 indicated in the file, and sets parameters' default values from the class'
 signature.

-::
+.. doctest-skip::

     >>> from astropy.cosmology import FlatLambdaCDM
-    >>> cosmo = FlatLambdaCDM.read('[file name]')
+    >>> cosmo = FlatLambdaCDM.read('<file name>')
     >>> cosmo == Planck18
     True
+
+Reading and writing :class:`~astropy.cosmology.Cosmology` objects go through
+intermediate representations, often a dict or `~astropy.table.QTable` instance.
+These intermediate representations are accessible through the methods
+:meth:`~astropy.cosmology.Cosmology.to_format` /
+:meth:`~astropy.cosmology.Cosmology.from_format`.
+
+To see the a list of the available formats:
+
+    >>> from astropy.cosmology import Cosmology
+    >>> Cosmology.to_format.list_formats()
+    Format Read Write Auto-identify
+    ------- ---- ----- -------------
+    mapping  Yes   Yes           Yes
+
+This list will include both built-in and registered 3rd-party formats.
+
+:meth:`~astropy.cosmology.Cosmology.to_format` /
+:meth:`~astropy.cosmology.Cosmology.from_format` parse a Cosmology to/from
+another python object. This can be useful for e.g., iterating through an MCMC
+of cosmological parameters or printing out a cosmological model to a journal
+format, like latex or HTML. When 3rd party cosmology packages register with
+Astropy' Cosmology I/O, ``to/from_format`` can be used to convert cosmology
+instances between packages!
+
+.. EXAMPLE START: Planck18 to mapping and back
+
+.. code-block::
+
+    >>> from astropy.cosmology import Planck18
+    >>> cm = Planck18.to_format("mapping")
+    >>> cm
+    {'cosmology': <class 'astropy.cosmology.core.FlatLambdaCDM'>,
+     'name': 'Planck18',
+     'H0': <Quantity 67.66 km / (Mpc s)>,
+     'Om0': 0.30966,
+     ...
+
+Now this dict can be used to load a new cosmological instance identical
+to the ``Planck18`` cosmology from which it was created.
+
+.. code-block::
+
+    >>> from astropy.cosmology import Cosmology
+    >>> cosmo = Cosmology.from_format(cm, format="mapping")
+    >>> cosmo == Planck18
+    True
+
+.. EXAMPLE END
+
+
+
+.. _custom_cosmology_converters:
+
+Custom Cosmology To/From Formats
+================================
+
+Custom representation formats may also be registered into the Astropy Cosmology
+I/O framework for use by these methods. For details of the framework see
+:ref:`io_registry`.
+
+.. EXAMPLE START : custom to/from format
+
+As an example, the following is an implementation of an `astropy.table.Row`
+converter. Note that we can use other registered parsers -- here "mapping"
+-- to make the implementation much simpler.
+
+We start by defining the function to parse a `astropy.table.Row` into a
+`~astropy.cosmology.Cosmology`. This function should take 1 positional
+argument, the row object, and 2 keyword arguments, for how to handle
+extra metadata and which Cosmology class to use. Details about metadata
+treatment are in ``Cosmology.from_format.help("mapping")``.
+
+.. code-block:: python
+    :emphasize-lines: 12,13
+
+    >>> import copy
+    >>> from astropy.cosmology import Cosmology
+
+    >>> def from_table_row(row, *, move_to_meta=False, cosmology=None):
+    ...     # get name from column
+    ...     name = row['name'] if 'name' in row.columns else None
+    ...     meta = copy.deepcopy(row.meta)
+    ...     # turn row into mapping (dict of the arguments)
+    ...     mapping = dict(row)
+    ...     mapping["meta"] = meta
+    ...     # build cosmology from map
+    ...     return Cosmology.from_format(mapping,
+    ...                                  move_to_meta=move_to_meta, cosmology=cosmology)
+
+The next step is a function to perform the reverse operation: parse a
+`~astropy.cosmology.Cosmology` into a `~astropy.table.Row`. This function
+requires only the cosmology object and a ``*args`` to absorb unneeded
+information passed by `astropy.io.registry.UnifiedReadWrite` (which
+implements `astropy.cosmology.Cosmology.to_format`).
+
+.. code-block:: python
+    :emphasize-lines: 4
+
+    >>> from astropy.table import QTable
+
+    >>> def to_table_row(cosmology, *args):
+    ...     p = cosmology.to_format("mapping")
+    ...     p["cosmology"] = p["cosmology"].__qualname__  # as string
+    ...     meta = p.pop("meta")
+    ...     # package parameters into lists for Table parsing
+    ...     params = {k: [v] for k, v in p.items()}
+    ...     return QTable(params, meta=meta)[0]  # return row
+
+Last we write a function to help with format auto-identification and then
+register everything into `astropy.io.registry`.
+
+.. code-block:: python
+    :emphasize-lines: 11, 12, 13
+
+    >>> from astropy.cosmology import Cosmology
+    >>> from astropy.io import registry as io_registry
+    >>> from astropy.table import Row
+
+    >>> def row_identify(origin, format, *args, **kwargs):
+    ...     """Identify if object uses the Table format."""
+    ...     if origin == "read":
+    ...         return isinstance(args[1], Row) and (format in (None, "row"))
+    ...     return False
+
+    >>> io_registry.register_reader("row", Cosmology, from_table_row)
+    >>> io_registry.register_writer("row", Cosmology, to_table_row)
+    >>> io_registry.register_identifier("row", Cosmology, row_identify)
+
+Now the registered functions can be used in
+:meth:`astropy.cosmology.Cosmology.from_format` and
+:meth:`astropy.cosmology.Cosmology.to_format`.
+
+.. code-block:: python
+
+    >>> from astropy.cosmology import Planck18
+    >>> row = Planck18.to_format("row")
+    >>> row
+    <Row index=0>
+      cosmology     name        H0        Om0    Tcmb0    Neff    m_nu [3]    Ob0
+                           km / (Mpc s)                              eV
+        str13       str8     float64    float64 float64 float64   float64   float64
+    ------------- -------- ------------ ------- ------- ------- ----------- -------
+    FlatLambdaCDM Planck18        67.66 0.30966  2.7255   3.046 0.0 .. 0.06 0.04897
+
+    >>> cosmo = Cosmology.from_format(row)
+    >>> cosmo == Planck18  # test it round-trips
+    True
+
+
+.. doctest::
+    :hide:
+
+    >>> io_registry.unregister_reader("row", Cosmology)
+    >>> io_registry.unregister_writer("row", Cosmology)
+    >>> io_registry.unregister_identifier("row", Cosmology)
+    >>> try:
+    ...     io_registry.get_reader("row", Cosmology)
+    ... except io_registry.IORegistryError:
+    ...     pass
+
+.. EXAMPLE END

 .. _custom_cosmology_readers_writers:
@@ -80,35 +240,125 @@ Custom Cosmology Readers/Writers

 Custom ``read`` / ``write`` formats may be registered into the Astropy Cosmology
 I/O framework. For details of the framework see :ref:`io_registry`.
-As a quick example, outlining how to make a
-`~astropy.cosmology.Cosmology` <-> JSON serializer:
+
+.. EXAMPLE START : custom read/write
+
+As an example, in the following we will fully work out a
+`~astropy.cosmology.Cosmology` <-> JSON (de)serializer.
+Note that we can use other registered parsers -- here "mapping"
+-- to make the implementation much simpler.
+
+We start by defining the function to parse JSON into a
+`~astropy.cosmology.Cosmology`. This function should take 1 positional
+argument, the file object or file path. We will also pass kwargs through to
+:meth:`~astropy.cosmology.Cosmology.from_format`, which handles metadata
+and which Cosmology class to use. Details of are in
+``Cosmology.from_format.help("mapping")``.

 .. code-block:: python
+    :emphasize-lines: 9,12,17

-   import json
-   from astropy.cosmology import Cosmology
-   from astropy.io import registry as io_registry
+    >>> import json, os
+    >>> import astropy.units as u
+    >>> from astropy.cosmology import Cosmology

-   def read_json(filename, **kwargs):
-       cosmology = ...  # read and parse file
-       return cosmology
+    >>> def read_json(filename, **kwargs):
+    ...     # read file, from path-like or file-like
+    ...     if isinstance(filename, (str, bytes, os.PathLike)):
+    ...         with open(filename, "r") as file:
+    ...             data = file.read()
+    ...     else:  # file-like : this also handles errors in dumping
+    ...         data = filename.read()
+    ...     mapping = json.loads(data)  # parse json mappable to dict
+    ...     # deserialize Quantity
+    ...     for k, v in mapping.items():
+    ...         if isinstance(v, dict) and "value" in v and "unit" in v:
+    ...             mapping[k] = u.Quantity(v["value"], v["unit"])
+    ...     return Cosmology.from_format(mapping, **kwargs)
+
+
+The next step is a function to write a `~astropy.cosmology.Cosmology` to
+JSON. This function requires the cosmology object and a file object/path.
+We also require the boolean flag "overwrite" to set behavior for existing
+files. Note that `~astropy.units.Quantity` is not natively compatible with
+JSON. In both the ``write`` and ``read`` methods we have to create custom
+parsers.
+
+.. code-block:: python
+    :emphasize-lines: 2,15
+
+    >>> def write_json(cosmology, file, *, overwrite=False, **kwargs):
+    ...    data = cosmology.to_format("mapping")  # start by turning into dict
+    ...    data["cosmology"] = data["cosmology"].__name__  # change class field to str
+    ...    # serialize Quantity
+    ...    for k, v in data.items():
+    ...        if isinstance(v, u.Quantity):
+    ...            data[k] = {"value": v.value.tolist(),
+    ...                       "unit": str(v.unit)}
+    ...
+    ...    if isinstance(file, (str, bytes, os.PathLike)):
+    ...        # check that file exists and whether to overwrite.
+    ...        if os.path.exists(file) and not overwrite:
+    ...            raise IOError(f"{file} exists. Set 'overwrite' to write over.")
+    ...        with open(file, "w") as write_file:
+    ...            json.dump(data, write_file)
+    ...    else:
+    ...        json.dump(data, file)
+
+Last we write a function to help with format auto-identification and then
+register everything into `astropy.io.registry`.
+
+.. code-block:: python
+    :emphasize-lines: 7,8,9

-   def write_json(cosmology, file, **kwargs):
-       data = ...  # parse cosmology to dict
-       with open(file, "w") as write_file:
-            json.dump(data, write_file)
+    >>> from astropy.io import registry as io_registry

-   def json_identify(origin, filepath, fileobj, *args, **kwargs):
-       """Identify if object uses the JSON format."""
-       return filepath is not None and filepath.endswith(".json")
+    >>> def json_identify(origin, filepath, fileobj, *args, **kwargs):
+    ...     """Identify if object uses the JSON format."""
+    ...     return filepath is not None and filepath.endswith(".json")

-   # register the read/write methods
-   io_registry.register_reader("json", Cosmology, read_json)
-   io_registry.register_writer("json", Cosmology, write_json)
-   io_registry.register_identifier("json", Cosmology, json_identify)
+    >>> io_registry.register_reader("json", Cosmology, read_json)
+    >>> io_registry.register_writer("json", Cosmology, write_json)
+    >>> io_registry.register_identifier("json", Cosmology, json_identify)
+
+Now the registered functions can be used in
+:meth:`astropy.cosmology.Cosmology.read` and
+:meth:`astropy.cosmology.Cosmology.write`.

+.. doctest-skip:: win32
+
+    >>> import tempfile
+    >>> from astropy.cosmology import Planck18
+    >>>
+    >>> file = tempfile.NamedTemporaryFile()
+    >>> Planck18.write(file.name, format="json", overwrite=True)
+    >>> with open(file.name) as f: f.readlines()
+    ['{"cosmology": "FlatLambdaCDM", "name": "Planck18",
+       "H0": {"value": 67.66, "unit": "km / (Mpc s)"}, "Om0": 0.30966,
+       ...
+    >>>
+    >>> cosmo = Cosmology.read(file.name, format="json")
+    >>> file.close()
+    >>> cosmo == Planck18  # test it round-trips
+    True
+
+
+.. doctest::
+    :hide:
+
+    >>> io_registry.unregister_reader("json", Cosmology)
+    >>> io_registry.unregister_writer("json", Cosmology)
+    >>> io_registry.unregister_identifier("json", Cosmology)
+    >>> try:
+    ...     io_registry.get_reader("json", Cosmology)
+    ... except io_registry.IORegistryError:
+    ...     pass
+
+.. EXAMPLE END

 Reference/API
 =============

 .. automodapi:: astropy.cosmology.connect
+
+.. automodapi:: astropy.cosmology.io.mapping
diff --git a/docs/whatsnew/5.0.rst b/docs/whatsnew/5.0.rst
index b8c135bb606f..a224b149c972 100644
--- a/docs/whatsnew/5.0.rst
+++ b/docs/whatsnew/5.0.rst
@@ -9,6 +9,47 @@ What's New in Astropy 5.0?
 Overview
 ========

+.. _whatsnew-5.0-cosmology:
+
+Support for reading, writing, and converting ``Cosmology``
+==========================================================
+
+Four new methods -- ``read``, ``write``, ``to_format``, ``from_format`` -- have
+been added to the ``Cosmology`` class, enabling reading from and writing to
+files and converting between different python objects.
+The methods use Astropy's Unified I/O registry so custom formats can be
+registered. Details are discussed in an addition to the docs.
+
+Currently no file formats are registered, but the syntax is as follows:
+
+.. doctest-skip::
+
+    >>> from astropy.cosmology import Planck18
+    >>> Planck18.write('<file name>.<format>')
+    >>>
+    >>> from astropy.cosmology import Cosmology
+    >>> cosmo = Cosmology.read('<file name>.<format>')
+    >>> cosmo == Planck18
+    True
+
+
+The transformation between ``Cosmology`` and `dict` is pre-registered,
+e.g. enabling::
+
+    >>> from astropy.cosmology import Planck18
+    >>> cm = Planck18.to_format("mapping")
+    >>> cm
+    {'cosmology': <class 'astropy.cosmology.core.FlatLambdaCDM'>,
+     'name': 'Planck18',
+     'H0': <Quantity 67.66 km / (Mpc s)>,
+     'Om0': 0.30966,
+     ...
+
+    >>> from astropy.cosmology import Cosmology
+    >>> cosmo = Cosmology.from_format(cm, format="mapping")
+    >>> cosmo == Planck18
+    True
+

 Full change log
 ===============

diff --git a/astropy/cosmology/io/tests/__init__.py b/astropy/cosmology/io/tests/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/astropy/cosmology/io/tests/test_mapping.py b/astropy/cosmology/io/tests/test_mapping.py
new file mode 100644
index 000000000000..df584ecc1ed5
--- /dev/null
+++ b/astropy/cosmology/io/tests/test_mapping.py
@@ -0,0 +1,79 @@
+# Licensed under a 3-clause BSD style license - see LICENSE.rst
+
+import pytest
+
+from astropy import cosmology
+from astropy.cosmology import Cosmology
+from astropy.cosmology.core import _COSMOLOGY_CLASSES
+from astropy.table import QTable, vstack
+from astropy.utils.compat import optional_deps
+from astropy.utils.exceptions import AstropyUserWarning
+
+
+class CosmologyWithKwargs(Cosmology):
+    def __init__(self, name="cosmology with kwargs", meta=None, **kwargs):
+        super().__init__(name=name, meta=meta, **kwargs)
+
+
+cosmo_instances = [
+    getattr(cosmology.realizations, name) for name in cosmology.parameters.available
+]
+cosmo_instances.append(CosmologyWithKwargs())
+
+
+def teardown_module(module):
+    # pop CosmologyWithKwargs from registered classes
+    # but don't error b/c it fails in parallel
+    _COSMOLOGY_CLASSES.pop(CosmologyWithKwargs.__qualname__, None)
+
+
+###############################################################################
+
+@pytest.mark.parametrize("expected", cosmo_instances)
+def test_to_from_mapping_instance(expected):
+    # ------------
+    # To Mapping
+    params = expected.to_format('mapping')
+
+    assert isinstance(params, dict)
+    assert params["cosmology"] is expected.__class__
+    assert params["name"] == expected.name
+
+    # ------------
+    # From Mapping
+    params["mismatching"] = "will error"
+
+    # tests are different if the last argument is a **kwarg
+    if tuple(expected._init_signature.parameters.values())[-1].kind == 4:
+        got = Cosmology.from_format(params, format="mapping")
+
+        assert got.__class__ == expected.__class__
+        assert got.name == expected.name
+        assert "mismatching" not in got.meta
+
+        return  # don't continue testing
+
+    # read with mismatching parameters errors
+    with pytest.raises(TypeError, match="there are unused parameters"):
+        Cosmology.from_format(params, format="mapping")
+
+    # unless mismatched are moved to meta
+    got = Cosmology.from_format(params, format="mapping", move_to_meta=True)
+    assert got.__class__ == expected.__class__
+    assert got == expected
+    assert got.meta["mismatching"] == "will error"
+
+    # it won't error if everything matches up
+    params.pop("mismatching")
+    got = Cosmology.from_format(params, format="mapping")
+    assert got.__class__ == expected.__class__
+    assert got == expected
+
+    # and it will also work if the cosmology is a string
+    params["cosmology"] = params["cosmology"].__name__
+    got = Cosmology.from_format(params, format="mapping")
+    assert got == expected
+
+    # also it auto-identifies 'format'
+    got = Cosmology.from_format(params)
+    assert got == expected
diff --git a/astropy/cosmology/tests/test_connect.py b/astropy/cosmology/tests/test_connect.py
index 49620acea346..e2f7e6c282d8 100644
--- a/astropy/cosmology/tests/test_connect.py
+++ b/astropy/cosmology/tests/test_connect.py
@@ -1,6 +1,7 @@
 # Licensed under a 3-clause BSD style license - see LICENSE.rst

 import copy
+import inspect
 import json
 import os

@@ -16,46 +17,14 @@
 from astropy.io import registry as io_registry

 cosmo_instances = cosmology.parameters.available
-save_formats = ["json"]
+readwrite_formats = ["json"]
+tofrom_formats = [("mapping", dict)]  # (format, data type)

 ###############################################################################
 # Setup

-def from_mapping(map, **kwargs):
-    params = copy.deepcopy(map)  # so can pop
-
-    # get cosmology. if string, parse to class
-    # 1st from 'kwargs'. Allows for override of the cosmology, if on file.
-    # 2nd from params. This MUST have the cosmology if 'kwargs' did not.
-    if "cosmology" in kwargs:
-        cosmology = kwargs.pop("cosmology")
-    else:
-        cosmology = params.pop("cosmology")
-    if isinstance(cosmology, str):
-        cosmology = _COSMOLOGY_CLASSES[cosmology]
-
-    # select arguments from mapping that are in the cosmo's signature.
-    ba = cosmology._init_signature.bind_partial()  # blank set of args
-    ba.apply_defaults()  # fill in the defaults
-    for k in cosmology._init_signature.parameters.keys():  # iter thru sig
-        if k in params:  # transfer argument, if in params
-            ba.arguments[k] = params.pop(k)
-
-    return cosmology(*ba.args, **ba.kwargs)
-
-
-def to_mapping(cosmology, *args, **kwargs):
-    m = {}
-    m["cosmology"] = cosmology.__class__
-    m.update({k: v for k, v in cosmology._init_arguments.items()
-              if k not in ("meta",)})
-    m["meta"] = copy.deepcopy(cosmology.meta)
-
-    return m
-
-
-def read_json(filename, key=None, **kwargs):
+def read_json(filename, **kwargs):
     with open(filename, "r") as file:
         data = file.read()
     mapping = json.loads(data)  # parse json mappable to dict
@@ -63,11 +32,19 @@ def read_json(filename, key=None, **kwargs):
     for k, v in mapping.items():
         if isinstance(v, dict) and "value" in v and "unit" in v:
             mapping[k] = u.Quantity(v["value"], v["unit"])
-    return from_mapping(mapping, **kwargs)
+    return Cosmology.from_format(mapping, **kwargs)

-def write_json(cosmology, file, *, overwrite=False, **kwargs):
-    data = to_mapping(cosmology)  # start by turning into dict
+def write_json(cosmology, file, *, overwrite=False):
+    """Write Cosmology to JSON.
+
+    Parameters
+    ----------
+    cosmology : `astropy.cosmology.Cosmology` subclass instance
+    file : path-like or file-like
+    overwrite : bool (optional, keyword-only)
+    """
+    data = cosmology.to_format("mapping")  # start by turning into dict
     data["cosmology"] = data["cosmology"].__name__  # change class field to str
     # serialize Quantity
     for k, v in data.items():
@@ -95,10 +72,9 @@ def setup_module(module):

 def teardown_module(module):
     """clean up module after tests."""
-    with pytest.warns(FutureWarning):  # idk
-        io_registry.unregister_reader("json", Cosmology)
-        io_registry.unregister_writer("json", Cosmology)
-        io_registry.unregister_identifier("json", Cosmology)
+    io_registry.unregister_reader("json", Cosmology)
+    io_registry.unregister_writer("json", Cosmology)
+    io_registry.unregister_identifier("json", Cosmology)

 ###############################################################################
@@ -106,7 +82,17 @@ def teardown_module(module):

 class TestReadWriteCosmology:

-    @pytest.mark.parametrize("format", save_formats)
+    @pytest.mark.parametrize("format", readwrite_formats)
+    def test_write_methods_have_explicit_kwarg_overwrite(self, format):
+        writer = io_registry.get_writer(format, Cosmology)
+        # test in signature
+        sig = inspect.signature(writer)
+        assert "overwrite" in sig.parameters
+
+        # also in docstring
+        assert "overwrite : bool" in writer.__doc__
+
+    @pytest.mark.parametrize("format", readwrite_formats)
     @pytest.mark.parametrize("instance", cosmo_instances)
     def test_complete_info(self, tmpdir, instance, format):
         """
@@ -132,7 +118,7 @@ def test_complete_info(self, tmpdir, instance, format):
         assert got == cosmo
         assert got.meta == cosmo.meta

-    @pytest.mark.parametrize("format", save_formats)
+    @pytest.mark.parametrize("format", readwrite_formats)
     @pytest.mark.parametrize("instance", cosmo_instances)
     def test_from_subclass_complete_info(self, tmpdir, instance, format):
         """
@@ -200,7 +186,7 @@ def test_from_subclass_partial_info(self, tmpdir, instance):
         # but the metadata is the same
         assert got.meta == cosmo.meta

-    @pytest.mark.parametrize("format", save_formats)
+    @pytest.mark.parametrize("format", readwrite_formats)
     @pytest.mark.parametrize("instance", cosmo_instances)
     def test_reader_class_mismatch(self, tmpdir, instance, format):
         """Test when the reader class doesn't match the file."""
@@ -219,3 +205,118 @@ def test_reader_class_mismatch(self, tmpdir, instance, format):
         # when specifying the class
         with pytest.raises(ValueError, match="`cosmology` must be either"):
             w0wzCDM.read(fname, format=format, cosmology="FlatLambdaCDM")
+
+
+class TestCosmologyToFromFormat:
+    """Test methods ``astropy.cosmology.Cosmology.to/from_format``."""
+
+    @pytest.mark.parametrize("format_type", tofrom_formats)
+    @pytest.mark.parametrize("instance", cosmo_instances)
+    def test_format_complete_info(self, instance, format_type):
+        """Read tests happen later."""
+        format, objtype = format_type
+        cosmo = getattr(cosmology.realizations, instance)
+
+        # test to_format
+        obj = cosmo.to_format(format)
+        assert isinstance(obj, objtype)
+
+        # test from_format
+        got = Cosmology.from_format(obj, format=format)
+        # and autodetect
+        got2 = Cosmology.from_format(obj)
+
+        assert got2 == got  # internal consistency
+        assert got == cosmo  # external consistency
+        assert got.meta == cosmo.meta
+
+    @pytest.mark.parametrize("format_type", tofrom_formats)
+    @pytest.mark.parametrize("instance", cosmo_instances)
+    def test_from_subclass_complete_info(self, instance, format_type):
+        """
+        Test transforming an instance and parsing from that class, when there's
+        full information available.
+        """
+        format, objtype = format_type
+        cosmo = getattr(cosmology.realizations, instance)
+
+        # test to_format
+        obj = cosmo.to_format(format)
+        assert isinstance(obj, objtype)
+
+        # read with the same class that wrote.
+        got = cosmo.__class__.from_format(obj, format=format)
+        got2 = Cosmology.from_format(obj)  # and autodetect
+
+        assert got2 == got  # internal consistency
+        assert got == cosmo  # external consistency
+        assert got.meta == cosmo.meta
+
+        # this should be equivalent to
+        got = Cosmology.from_format(obj, format=format, cosmology=cosmo.__class__)
+        assert got == cosmo
+        assert got.meta == cosmo.meta
+
+        # and also
+        got = Cosmology.from_format(obj, format=format, cosmology=cosmo.__class__.__qualname__)
+        assert got == cosmo
+        assert got.meta == cosmo.meta
+
+    @pytest.mark.parametrize("instance", cosmo_instances)
+    def test_from_subclass_partial_info(self, instance):
+        """
+        Test writing from an instance and reading from that class.
+        This requires partial information.
+
+        .. todo::
+
+            generalize over all formats for this test.
+        """
+        format, objtype = ("mapping", dict)
+        cosmo = getattr(cosmology.realizations, instance)
+
+        # test to_format
+        obj = cosmo.to_format(format)
+        assert isinstance(obj, objtype)
+
+        # partial information
+        tempobj = copy.deepcopy(obj)
+        del tempobj["cosmology"]
+        del tempobj["Tcmb0"]
+
+        # read with the same class that wrote fills in the missing info with
+        # the default value
+        got = cosmo.__class__.from_format(tempobj, format=format)
+        got2 = Cosmology.from_format(tempobj, format=format, cosmology=cosmo.__class__)
+        got3 = Cosmology.from_format(tempobj, format=format, cosmology=cosmo.__class__.__qualname__)
+
+        assert (got == got2) and (got2 == got3)  # internal consistency
+
+        # not equal, because Tcmb0 is changed
+        assert got != cosmo
+        assert got.Tcmb0 == cosmo.__class__._init_signature.parameters["Tcmb0"].default
+        assert got.clone(name=cosmo.name, Tcmb0=cosmo.Tcmb0.value) == cosmo
+        # but the metadata is the same
+        assert got.meta == cosmo.meta
+
+    @pytest.mark.parametrize("format_type", tofrom_formats)
+    @pytest.mark.parametrize("instance", cosmo_instances)
+    def test_reader_class_mismatch(self, instance, format_type):
+        """Test when the reader class doesn't match the object."""
+        format, objtype = format_type
+        cosmo = getattr(cosmology.realizations, instance)
+
+        # test to_format
+        obj = cosmo.to_format(format)
+        assert isinstance(obj, objtype)
+
+        # class mismatch
+        with pytest.raises(TypeError, match="missing 1 required"):
+            w0wzCDM.from_format(obj, format=format)
+
+        with pytest.raises(TypeError, match="missing 1 required"):
+            Cosmology.from_format(obj, format=format, cosmology=w0wzCDM)
+
+        # when specifying the class
+        with pytest.raises(ValueError, match="`cosmology` must be either"):
+            w0wzCDM.from_format(obj, format=format, cosmology="FlatLambdaCDM")

diff --git a/docs/changes/cosmology/#####.feature.rst b/docs/changes/cosmology/#####.feature.rst
new file mode 100644
index 0000000000..df78f84a93
--- /dev/null
+++ b/docs/changes/cosmology/#####.feature.rst
@@ -0,0 +1,5 @@
+Added ``to_format/from_format`` methods to Cosmology using the Unified I/O
+registry. Now custom format converters and format-identifier functions
+can be registered to transform Cosmology objects.
+The transformation between Cosmology and dictionaries is pre-registered.
+Details are discussed in an addition to the docs.

diff --git a/docs/cosmology/io.rst b/docs/cosmology/io.rst
index bb9f1881b4..db5f9509a2 100644
--- a/docs/cosmology/io.rst
+++ b/docs/cosmology/io.rst
@@ -1,25 +1,22 @@
-.. doctest-skip-all
-
 .. _read_write_cosmologies:

-Reading and Writing Cosmology Objects
-*************************************
+Read, Write, and Convert Cosmology Objects
+******************************************

 An easy way to serialize and deserialize a Cosmology object is using the
 :mod:`pickle` module.

-.. code-block:: python
-   :emphasize-lines: 1,4,8
+.. doctest-skip::

    >>> import pickle
    >>> from astropy.cosmology import Planck18
    >>> with open("planck18.pkl", mode="wb") as file:
-   ...     pickle.dump(file, Planck18)
-
-   And to read the file back
+   ...     pickle.dump(Planck18, file)
+   >>> # and to read back
    >>> with open("planck18.pkl", mode="rb") as file:
-   >>>     cosmo = pickle.load(file)
+   ...     cosmo = pickle.load(file)
    >>> cosmo
+   FlatLambdaCDM(name="Planck18", ...

 However this method has all the attendant drawbacks of :mod:`pickle` — security
 vulnerabilities and non-human-readable files.
@@ -43,34 +40,197 @@ framework.
 Writing a cosmology instance requires only the file location and optionally,
 if the file format cannot be inferred, a keyword argument "format". Additional
 positional arguments and keyword arguments are passed to the reader methods.
-::
+
+.. doctest-skip::

     >>> from astropy.cosmology import Planck18
-    >>> Planck18.write('[file name]')
+    >>> Planck18.write('<file name>')

 Reading back the cosmology is most safely done from ``Cosmology``, the base
 class, as it provides no default information and therefore requires the file
 to have all necessary information to describe a cosmology.

-::
+.. doctest-skip::

     >>> from astropy.cosmology import Cosmology
-    >>> cosmo = Cosmology.read('[file name]')
+    >>> cosmo = Cosmology.read('<file name>')
     >>> cosmo == Planck18
     True

 When a subclass of ``Cosmology`` is used to read a file, the subclass will
-provide a keyword argument ``cosmology=[class]`` to the registered read
+provide a keyword argument ``cosmology=<class>`` to the registered read
 method. The method uses this cosmology class, regardless of the class
 indicated in the file, and sets parameters' default values from the class'
 signature.

-::
+.. doctest-skip::

     >>> from astropy.cosmology import FlatLambdaCDM
-    >>> cosmo = FlatLambdaCDM.read('[file name]')
+    >>> cosmo = FlatLambdaCDM.read('<file name>')
     >>> cosmo == Planck18
     True
+
+Reading and writing :class:`~astropy.cosmology.Cosmology` objects go through
+intermediate representations, often a dict or `~astropy.table.QTable` instance.
+These intermediate representations are accessible through the methods
+:meth:`~astropy.cosmology.Cosmology.to_format` /
+:meth:`~astropy.cosmology.Cosmology.from_format`.
+
+To see the a list of the available formats:
+
+    >>> from astropy.cosmology import Cosmology
+    >>> Cosmology.to_format.list_formats()
+    Format Read Write Auto-identify
+    ------- ---- ----- -------------
+    mapping  Yes   Yes           Yes
+
+This list will include both built-in and registered 3rd-party formats.
+
+:meth:`~astropy.cosmology.Cosmology.to_format` /
+:meth:`~astropy.cosmology.Cosmology.from_format` parse a Cosmology to/from
+another python object. This can be useful for e.g., iterating through an MCMC
+of cosmological parameters or printing out a cosmological model to a journal
+format, like latex or HTML. When 3rd party cosmology packages register with
+Astropy' Cosmology I/O, ``to/from_format`` can be used to convert cosmology
+instances between packages!
+
+.. EXAMPLE START: Planck18 to mapping and back
+
+.. code-block::
+
+    >>> from astropy.cosmology import Planck18
+    >>> cm = Planck18.to_format("mapping")
+    >>> cm
+    {'cosmology': <class 'astropy.cosmology.core.FlatLambdaCDM'>,
+     'name': 'Planck18',
+     'H0': <Quantity 67.66 km / (Mpc s)>,
+     'Om0': 0.30966,
+     ...
+
+Now this dict can be used to load a new cosmological instance identical
+to the ``Planck18`` cosmology from which it was created.
+
+.. code-block::
+
+    >>> from astropy.cosmology import Cosmology
+    >>> cosmo = Cosmology.from_format(cm, format="mapping")
+    >>> cosmo == Planck18
+    True
+
+.. EXAMPLE END
+
+
+
+.. _custom_cosmology_converters:
+
+Custom Cosmology To/From Formats
+================================
+
+Custom representation formats may also be registered into the Astropy Cosmology
+I/O framework for use by these methods. For details of the framework see
+:ref:`io_registry`.
+
+.. EXAMPLE START : custom to/from format
+
+As an example, the following is an implementation of an `astropy.table.Row`
+converter. Note that we can use other registered parsers -- here "mapping"
+-- to make the implementation much simpler.
+
+We start by defining the function to parse a `astropy.table.Row` into a
+`~astropy.cosmology.Cosmology`. This function should take 1 positional
+argument, the row object, and 2 keyword arguments, for how to handle
+extra metadata and which Cosmology class to use. Details about metadata
+treatment are in ``Cosmology.from_format.help("mapping")``.
+
+.. code-block:: python
+    :emphasize-lines: 12,13
+
+    >>> import copy
+    >>> from astropy.cosmology import Cosmology
+
+    >>> def from_table_row(row, *, move_to_meta=False, cosmology=None):
+    ...     # get name from column
+    ...     name = row['name'] if 'name' in row.columns else None
+    ...     meta = copy.deepcopy(row.meta)
+    ...     # turn row into mapping (dict of the arguments)
+    ...     mapping = dict(row)
+    ...     mapping["meta"] = meta
+    ...     # build cosmology from map
+    ...     return Cosmology.from_format(mapping,
+    ...                                  move_to_meta=move_to_meta, cosmology=cosmology)
+
+The next step is a function to perform the reverse operation: parse a
+`~astropy.cosmology.Cosmology` into a `~astropy.table.Row`. This function
+requires only the cosmology object and a ``*args`` to absorb unneeded
+information passed by `astropy.io.registry.UnifiedReadWrite` (which
+implements `astropy.cosmology.Cosmology.to_format`).
+
+.. code-block:: python
+    :emphasize-lines: 4
+
+    >>> from astropy.table import QTable
+
+    >>> def to_table_row(cosmology, *args):
+    ...     p = cosmology.to_format("mapping")
+    ...     p["cosmology"] = p["cosmology"].__qualname__  # as string
+    ...     meta = p.pop("meta")
+    ...     # package parameters into lists for Table parsing
+    ...     params = {k: [v] for k, v in p.items()}
+    ...     return QTable(params, meta=meta)[0]  # return row
+
+Last we write a function to help with format auto-identification and then
+register everything into `astropy.io.registry`.
+
+.. code-block:: python
+    :emphasize-lines: 11, 12, 13
+
+    >>> from astropy.cosmology import Cosmology
+    >>> from astropy.io import registry as io_registry
+    >>> from astropy.table import Row
+
+    >>> def row_identify(origin, format, *args, **kwargs):
+    ...     """Identify if object uses the Table format."""
+    ...     if origin == "read":
+    ...         return isinstance(args[1], Row) and (format in (None, "row"))
+    ...     return False
+
+    >>> io_registry.register_reader("row", Cosmology, from_table_row)
+    >>> io_registry.register_writer("row", Cosmology, to_table_row)
+    >>> io_registry.register_identifier("row", Cosmology, row_identify)
+
+Now the registered functions can be used in
+:meth:`astropy.cosmology.Cosmology.from_format` and
+:meth:`astropy.cosmology.Cosmology.to_format`.
+
+.. code-block:: python
+
+    >>> from astropy.cosmology import Planck18
+    >>> row = Planck18.to_format("row")
+    >>> row
+    <Row index=0>
+      cosmology     name        H0        Om0    Tcmb0    Neff    m_nu [3]    Ob0
+                           km / (Mpc s)                              eV
+        str13       str8     float64    float64 float64 float64   float64   float64
+    ------------- -------- ------------ ------- ------- ------- ----------- -------
+    FlatLambdaCDM Planck18        67.66 0.30966  2.7255   3.046 0.0 .. 0.06 0.04897
+
+    >>> cosmo = Cosmology.from_format(row)
+    >>> cosmo == Planck18  # test it round-trips
+    True
+
+
+.. doctest::
+    :hide:
+
+    >>> io_registry.unregister_reader("row", Cosmology)
+    >>> io_registry.unregister_writer("row", Cosmology)
+    >>> io_registry.unregister_identifier("row", Cosmology)
+    >>> try:
+    ...     io_registry.get_reader("row", Cosmology)
+    ... except io_registry.IORegistryError:
+    ...     pass
+
+.. EXAMPLE END

 .. _custom_cosmology_readers_writers:
@@ -80,35 +240,125 @@ Custom Cosmology Readers/Writers

 Custom ``read`` / ``write`` formats may be registered into the Astropy Cosmology
 I/O framework. For details of the framework see :ref:`io_registry`.
-As a quick example, outlining how to make a
-`~astropy.cosmology.Cosmology` <-> JSON serializer:
+
+.. EXAMPLE START : custom read/write
+
+As an example, in the following we will fully work out a
+`~astropy.cosmology.Cosmology` <-> JSON (de)serializer.
+Note that we can use other registered parsers -- here "mapping"
+-- to make the implementation much simpler.
+
+We start by defining the function to parse JSON into a
+`~astropy.cosmology.Cosmology`. This function should take 1 positional
+argument, the file object or file path. We will also pass kwargs through to
+:meth:`~astropy.cosmology.Cosmology.from_format`, which handles metadata
+and which Cosmology class to use. Details of are in
+``Cosmology.from_format.help("mapping")``.

 .. code-block:: python
+    :emphasize-lines: 9,12,17

-   import json
-   from astropy.cosmology import Cosmology
-   from astropy.io import registry as io_registry
+    >>> import json, os
+    >>> import astropy.units as u
+    >>> from astropy.cosmology import Cosmology

-   def read_json(filename, **kwargs):
-       cosmology = ...  # read and parse file
-       return cosmology
+    >>> def read_json(filename, **kwargs):
+    ...     # read file, from path-like or file-like
+    ...     if isinstance(filename, (str, bytes, os.PathLike)):
+    ...         with open(filename, "r") as file:
+    ...             data = file.read()
+    ...     else:  # file-like : this also handles errors in dumping
+    ...         data = filename.read()
+    ...     mapping = json.loads(data)  # parse json mappable to dict
+    ...     # deserialize Quantity
+    ...     for k, v in mapping.items():
+    ...         if isinstance(v, dict) and "value" in v and "unit" in v:
+    ...             mapping[k] = u.Quantity(v["value"], v["unit"])
+    ...     return Cosmology.from_format(mapping, **kwargs)
+
+
+The next step is a function to write a `~astropy.cosmology.Cosmology` to
+JSON. This function requires the cosmology object and a file object/path.
+We also require the boolean flag "overwrite" to set behavior for existing
+files. Note that `~astropy.units.Quantity` is not natively compatible with
+JSON. In both the ``write`` and ``read`` methods we have to create custom
+parsers.
+
+.. code-block:: python
+    :emphasize-lines: 2,15
+
+    >>> def write_json(cosmology, file, *, overwrite=False, **kwargs):
+    ...    data = cosmology.to_format("mapping")  # start by turning into dict
+    ...    data["cosmology"] = data["cosmology"].__name__  # change class field to str
+    ...    # serialize Quantity
+    ...    for k, v in data.items():
+    ...        if isinstance(v, u.Quantity):
+    ...            data[k] = {"value": v.value.tolist(),
+    ...                       "unit": str(v.unit)}
+    ...
+    ...    if isinstance(file, (str, bytes, os.PathLike)):
+    ...        # check that file exists and whether to overwrite.
+    ...        if os.path.exists(file) and not overwrite:
+    ...            raise IOError(f"{file} exists. Set 'overwrite' to write over.")
+    ...        with open(file, "w") as write_file:
+    ...            json.dump(data, write_file)
+    ...    else:
+    ...        json.dump(data, file)
+
+Last we write a function to help with format auto-identification and then
+register everything into `astropy.io.registry`.
+
+.. code-block:: python
+    :emphasize-lines: 7,8,9

-   def write_json(cosmology, file, **kwargs):
-       data = ...  # parse cosmology to dict
-       with open(file, "w") as write_file:
-            json.dump(data, write_file)
+    >>> from astropy.io import registry as io_registry

-   def json_identify(origin, filepath, fileobj, *args, **kwargs):
-       """Identify if object uses the JSON format."""
-       return filepath is not None and filepath.endswith(".json")
+    >>> def json_identify(origin, filepath, fileobj, *args, **kwargs):
+    ...     """Identify if object uses the JSON format."""
+    ...     return filepath is not None and filepath.endswith(".json")

-   # register the read/write methods
-   io_registry.register_reader("json", Cosmology, read_json)
-   io_registry.register_writer("json", Cosmology, write_json)
-   io_registry.register_identifier("json", Cosmology, json_identify)
+    >>> io_registry.register_reader("json", Cosmology, read_json)
+    >>> io_registry.register_writer("json", Cosmology, write_json)
+    >>> io_registry.register_identifier("json", Cosmology, json_identify)
+
+Now the registered functions can be used in
+:meth:`astropy.cosmology.Cosmology.read` and
+:meth:`astropy.cosmology.Cosmology.write`.

+.. doctest-skip:: win32
+
+    >>> import tempfile
+    >>> from astropy.cosmology import Planck18
+    >>>
+    >>> file = tempfile.NamedTemporaryFile()
+    >>> Planck18.write(file.name, format="json", overwrite=True)
+    >>> with open(file.name) as f: f.readlines()
+    ['{"cosmology": "FlatLambdaCDM", "name": "Planck18",
+       "H0": {"value": 67.66, "unit": "km / (Mpc s)"}, "Om0": 0.30966,
+       ...
+    >>>
+    >>> cosmo = Cosmology.read(file.name, format="json")
+    >>> file.close()
+    >>> cosmo == Planck18  # test it round-trips
+    True
+
+
+.. doctest::
+    :hide:
+
+    >>> io_registry.unregister_reader("json", Cosmology)
+    >>> io_registry.unregister_writer("json", Cosmology)
+    >>> io_registry.unregister_identifier("json", Cosmology)
+    >>> try:
+    ...     io_registry.get_reader("json", Cosmology)
+    ... except io_registry.IORegistryError:
+    ...     pass
+
+.. EXAMPLE END

 Reference/API
 =============

 .. automodapi:: astropy.cosmology.connect
+
+.. automodapi:: astropy.cosmology.io.mapping

diff --git a/docs/whatsnew/5.0.rst b/docs/whatsnew/5.0.rst
index b8c135bb60..a224b149c9 100644
--- a/docs/whatsnew/5.0.rst
+++ b/docs/whatsnew/5.0.rst
@@ -9,6 +9,47 @@ What's New in Astropy 5.0?
 Overview
 ========

+.. _whatsnew-5.0-cosmology:
+
+Support for reading, writing, and converting ``Cosmology``
+==========================================================
+
+Four new methods -- ``read``, ``write``, ``to_format``, ``from_format`` -- have
+been added to the ``Cosmology`` class, enabling reading from and writing to
+files and converting between different python objects.
+The methods use Astropy's Unified I/O registry so custom formats can be
+registered. Details are discussed in an addition to the docs.
+
+Currently no file formats are registered, but the syntax is as follows:
+
+.. doctest-skip::
+
+    >>> from astropy.cosmology import Planck18
+    >>> Planck18.write('<file name>.<format>')
+    >>>
+    >>> from astropy.cosmology import Cosmology
+    >>> cosmo = Cosmology.read('<file name>.<format>')
+    >>> cosmo == Planck18
+    True
+
+
+The transformation between ``Cosmology`` and `dict` is pre-registered,
+e.g. enabling::
+
+    >>> from astropy.cosmology import Planck18
+    >>> cm = Planck18.to_format("mapping")
+    >>> cm
+    {'cosmology': <class 'astropy.cosmology.core.FlatLambdaCDM'>,
+     'name': 'Planck18',
+     'H0': <Quantity 67.66 km / (Mpc s)>,
+     'Om0': 0.30966,
+     ...
+
+    >>> from astropy.cosmology import Cosmology
+    >>> cosmo = Cosmology.from_format(cm, format="mapping")
+    >>> cosmo == Planck18
+    True
+

 Full change log
 ===============

diff --git a/docs/changes/cosmology/11998.feature.rst b/docs/changes/cosmology/11998.feature.rst
new file mode 100644
index 000000000000..df78f84a93c9
--- /dev/null
+++ b/docs/changes/cosmology/11998.feature.rst
@@ -0,0 +1,5 @@
+Added ``to_format/from_format`` methods to Cosmology using the Unified I/O
+registry. Now custom format converters and format-identifier functions
+can be registered to transform Cosmology objects.
+The transformation between Cosmology and dictionaries is pre-registered.
+Details are discussed in an addition to the docs.

diff --git a/docs/cosmology/io.rst b/docs/cosmology/io.rst
index bb9f1881b4d1..db5f9509a287 100644
--- a/docs/cosmology/io.rst
+++ b/docs/cosmology/io.rst
@@ -1,25 +1,22 @@
-.. doctest-skip-all
-
 .. _read_write_cosmologies:

-Reading and Writing Cosmology Objects
-*************************************
+Read, Write, and Convert Cosmology Objects
+******************************************

 An easy way to serialize and deserialize a Cosmology object is using the
 :mod:`pickle` module.

-.. code-block:: python
-   :emphasize-lines: 1,4,8
+.. doctest-skip::

    >>> import pickle
    >>> from astropy.cosmology import Planck18
    >>> with open("planck18.pkl", mode="wb") as file:
-   ...     pickle.dump(file, Planck18)
-
-   And to read the file back
+   ...     pickle.dump(Planck18, file)
+   >>> # and to read back
    >>> with open("planck18.pkl", mode="rb") as file:
-   >>>     cosmo = pickle.load(file)
+   ...     cosmo = pickle.load(file)
    >>> cosmo
+   FlatLambdaCDM(name="Planck18", ...

 However this method has all the attendant drawbacks of :mod:`pickle` — security
 vulnerabilities and non-human-readable files.
@@ -43,34 +40,197 @@ framework.
 Writing a cosmology instance requires only the file location and optionally,
 if the file format cannot be inferred, a keyword argument "format". Additional
 positional arguments and keyword arguments are passed to the reader methods.
-::
+
+.. doctest-skip::

     >>> from astropy.cosmology import Planck18
-    >>> Planck18.write('[file name]')
+    >>> Planck18.write('<file name>')

 Reading back the cosmology is most safely done from ``Cosmology``, the base
 class, as it provides no default information and therefore requires the file
 to have all necessary information to describe a cosmology.

-::
+.. doctest-skip::

     >>> from astropy.cosmology import Cosmology
-    >>> cosmo = Cosmology.read('[file name]')
+    >>> cosmo = Cosmology.read('<file name>')
     >>> cosmo == Planck18
     True

 When a subclass of ``Cosmology`` is used to read a file, the subclass will
-provide a keyword argument ``cosmology=[class]`` to the registered read
+provide a keyword argument ``cosmology=<class>`` to the registered read
 method. The method uses this cosmology class, regardless of the class
 indicated in the file, and sets parameters' default values from the class'
 signature.

-::
+.. doctest-skip::

     >>> from astropy.cosmology import FlatLambdaCDM
-    >>> cosmo = FlatLambdaCDM.read('[file name]')
+    >>> cosmo = FlatLambdaCDM.read('<file name>')
     >>> cosmo == Planck18
     True
+
+Reading and writing :class:`~astropy.cosmology.Cosmology` objects go through
+intermediate representations, often a dict or `~astropy.table.QTable` instance.
+These intermediate representations are accessible through the methods
+:meth:`~astropy.cosmology.Cosmology.to_format` /
+:meth:`~astropy.cosmology.Cosmology.from_format`.
+
+To see the a list of the available formats:
+
+    >>> from astropy.cosmology import Cosmology
+    >>> Cosmology.to_format.list_formats()
+    Format Read Write Auto-identify
+    ------- ---- ----- -------------
+    mapping  Yes   Yes           Yes
+
+This list will include both built-in and registered 3rd-party formats.
+
+:meth:`~astropy.cosmology.Cosmology.to_format` /
+:meth:`~astropy.cosmology.Cosmology.from_format` parse a Cosmology to/from
+another python object. This can be useful for e.g., iterating through an MCMC
+of cosmological parameters or printing out a cosmological model to a journal
+format, like latex or HTML. When 3rd party cosmology packages register with
+Astropy' Cosmology I/O, ``to/from_format`` can be used to convert cosmology
+instances between packages!
+
+.. EXAMPLE START: Planck18 to mapping and back
+
+.. code-block::
+
+    >>> from astropy.cosmology import Planck18
+    >>> cm = Planck18.to_format("mapping")
+    >>> cm
+    {'cosmology': <class 'astropy.cosmology.core.FlatLambdaCDM'>,
+     'name': 'Planck18',
+     'H0': <Quantity 67.66 km / (Mpc s)>,
+     'Om0': 0.30966,
+     ...
+
+Now this dict can be used to load a new cosmological instance identical
+to the ``Planck18`` cosmology from which it was created.
+
+.. code-block::
+
+    >>> from astropy.cosmology import Cosmology
+    >>> cosmo = Cosmology.from_format(cm, format="mapping")
+    >>> cosmo == Planck18
+    True
+
+.. EXAMPLE END
+
+
+
+.. _custom_cosmology_converters:
+
+Custom Cosmology To/From Formats
+================================
+
+Custom representation formats may also be registered into the Astropy Cosmology
+I/O framework for use by these methods. For details of the framework see
+:ref:`io_registry`.
+
+.. EXAMPLE START : custom to/from format
+
+As an example, the following is an implementation of an `astropy.table.Row`
+converter. Note that we can use other registered parsers -- here "mapping"
+-- to make the implementation much simpler.
+
+We start by defining the function to parse a `astropy.table.Row` into a
+`~astropy.cosmology.Cosmology`. This function should take 1 positional
+argument, the row object, and 2 keyword arguments, for how to handle
+extra metadata and which Cosmology class to use. Details about metadata
+treatment are in ``Cosmology.from_format.help("mapping")``.
+
+.. code-block:: python
+    :emphasize-lines: 12,13
+
+    >>> import copy
+    >>> from astropy.cosmology import Cosmology
+
+    >>> def from_table_row(row, *, move_to_meta=False, cosmology=None):
+    ...     # get name from column
+    ...     name = row['name'] if 'name' in row.columns else None
+    ...     meta = copy.deepcopy(row.meta)
+    ...     # turn row into mapping (dict of the arguments)
+    ...     mapping = dict(row)
+    ...     mapping["meta"] = meta
+    ...     # build cosmology from map
+    ...     return Cosmology.from_format(mapping,
+    ...                                  move_to_meta=move_to_meta, cosmology=cosmology)
+
+The next step is a function to perform the reverse operation: parse a
+`~astropy.cosmology.Cosmology` into a `~astropy.table.Row`. This function
+requires only the cosmology object and a ``*args`` to absorb unneeded
+information passed by `astropy.io.registry.UnifiedReadWrite` (which
+implements `astropy.cosmology.Cosmology.to_format`).
+
+.. code-block:: python
+    :emphasize-lines: 4
+
+    >>> from astropy.table import QTable
+
+    >>> def to_table_row(cosmology, *args):
+    ...     p = cosmology.to_format("mapping")
+    ...     p["cosmology"] = p["cosmology"].__qualname__  # as string
+    ...     meta = p.pop("meta")
+    ...     # package parameters into lists for Table parsing
+    ...     params = {k: [v] for k, v in p.items()}
+    ...     return QTable(params, meta=meta)[0]  # return row
+
+Last we write a function to help with format auto-identification and then
+register everything into `astropy.io.registry`.
+
+.. code-block:: python
+    :emphasize-lines: 11, 12, 13
+
+    >>> from astropy.cosmology import Cosmology
+    >>> from astropy.io import registry as io_registry
+    >>> from astropy.table import Row
+
+    >>> def row_identify(origin, format, *args, **kwargs):
+    ...     """Identify if object uses the Table format."""
+    ...     if origin == "read":
+    ...         return isinstance(args[1], Row) and (format in (None, "row"))
+    ...     return False
+
+    >>> io_registry.register_reader("row", Cosmology, from_table_row)
+    >>> io_registry.register_writer("row", Cosmology, to_table_row)
+    >>> io_registry.register_identifier("row", Cosmology, row_identify)
+
+Now the registered functions can be used in
+:meth:`astropy.cosmology.Cosmology.from_format` and
+:meth:`astropy.cosmology.Cosmology.to_format`.
+
+.. code-block:: python
+
+    >>> from astropy.cosmology import Planck18
+    >>> row = Planck18.to_format("row")
+    >>> row
+    <Row index=0>
+      cosmology     name        H0        Om0    Tcmb0    Neff    m_nu [3]    Ob0
+                           km / (Mpc s)                              eV
+        str13       str8     float64    float64 float64 float64   float64   float64
+    ------------- -------- ------------ ------- ------- ------- ----------- -------
+    FlatLambdaCDM Planck18        67.66 0.30966  2.7255   3.046 0.0 .. 0.06 0.04897
+
+    >>> cosmo = Cosmology.from_format(row)
+    >>> cosmo == Planck18  # test it round-trips
+    True
+
+
+.. doctest::
+    :hide:
+
+    >>> io_registry.unregister_reader("row", Cosmology)
+    >>> io_registry.unregister_writer("row", Cosmology)
+    >>> io_registry.unregister_identifier("row", Cosmology)
+    >>> try:
+    ...     io_registry.get_reader("row", Cosmology)
+    ... except io_registry.IORegistryError:
+    ...     pass
+
+.. EXAMPLE END

 .. _custom_cosmology_readers_writers:
@@ -80,35 +240,125 @@ Custom Cosmology Readers/Writers

 Custom ``read`` / ``write`` formats may be registered into the Astropy Cosmology
 I/O framework. For details of the framework see :ref:`io_registry`.
-As a quick example, outlining how to make a
-`~astropy.cosmology.Cosmology` <-> JSON serializer:
+
+.. EXAMPLE START : custom read/write
+
+As an example, in the following we will fully work out a
+`~astropy.cosmology.Cosmology` <-> JSON (de)serializer.
+Note that we can use other registered parsers -- here "mapping"
+-- to make the implementation much simpler.
+
+We start by defining the function to parse JSON into a
+`~astropy.cosmology.Cosmology`. This function should take 1 positional
+argument, the file object or file path. We will also pass kwargs through to
+:meth:`~astropy.cosmology.Cosmology.from_format`, which handles metadata
+and which Cosmology class to use. Details of are in
+``Cosmology.from_format.help("mapping")``.

 .. code-block:: python
+    :emphasize-lines: 9,12,17

-   import json
-   from astropy.cosmology import Cosmology
-   from astropy.io import registry as io_registry
+    >>> import json, os
+    >>> import astropy.units as u
+    >>> from astropy.cosmology import Cosmology

-   def read_json(filename, **kwargs):
-       cosmology = ...  # read and parse file
-       return cosmology
+    >>> def read_json(filename, **kwargs):
+    ...     # read file, from path-like or file-like
+    ...     if isinstance(filename, (str, bytes, os.PathLike)):
+    ...         with open(filename, "r") as file:
+    ...             data = file.read()
+    ...     else:  # file-like : this also handles errors in dumping
+    ...         data = filename.read()
+    ...     mapping = json.loads(data)  # parse json mappable to dict
+    ...     # deserialize Quantity
+    ...     for k, v in mapping.items():
+    ...         if isinstance(v, dict) and "value" in v and "unit" in v:
+    ...             mapping[k] = u.Quantity(v["value"], v["unit"])
+    ...     return Cosmology.from_format(mapping, **kwargs)
+
+
+The next step is a function to write a `~astropy.cosmology.Cosmology` to
+JSON. This function requires the cosmology object and a file object/path.
+We also require the boolean flag "overwrite" to set behavior for existing
+files. Note that `~astropy.units.Quantity` is not natively compatible with
+JSON. In both the ``write`` and ``read`` methods we have to create custom
+parsers.
+
+.. code-block:: python
+    :emphasize-lines: 2,15
+
+    >>> def write_json(cosmology, file, *, overwrite=False, **kwargs):
+    ...    data = cosmology.to_format("mapping")  # start by turning into dict
+    ...    data["cosmology"] = data["cosmology"].__name__  # change class field to str
+    ...    # serialize Quantity
+    ...    for k, v in data.items():
+    ...        if isinstance(v, u.Quantity):
+    ...            data[k] = {"value": v.value.tolist(),
+    ...                       "unit": str(v.unit)}
+    ...
+    ...    if isinstance(file, (str, bytes, os.PathLike)):
+    ...        # check that file exists and whether to overwrite.
+    ...        if os.path.exists(file) and not overwrite:
+    ...            raise IOError(f"{file} exists. Set 'overwrite' to write over.")
+    ...        with open(file, "w") as write_file:
+    ...            json.dump(data, write_file)
+    ...    else:
+    ...        json.dump(data, file)
+
+Last we write a function to help with format auto-identification and then
+register everything into `astropy.io.registry`.
+
+.. code-block:: python
+    :emphasize-lines: 7,8,9

-   def write_json(cosmology, file, **kwargs):
-       data = ...  # parse cosmology to dict
-       with open(file, "w") as write_file:
-            json.dump(data, write_file)
+    >>> from astropy.io import registry as io_registry

-   def json_identify(origin, filepath, fileobj, *args, **kwargs):
-       """Identify if object uses the JSON format."""
-       return filepath is not None and filepath.endswith(".json")
+    >>> def json_identify(origin, filepath, fileobj, *args, **kwargs):
+    ...     """Identify if object uses the JSON format."""
+    ...     return filepath is not None and filepath.endswith(".json")

-   # register the read/write methods
-   io_registry.register_reader("json", Cosmology, read_json)
-   io_registry.register_writer("json", Cosmology, write_json)
-   io_registry.register_identifier("json", Cosmology, json_identify)
+    >>> io_registry.register_reader("json", Cosmology, read_json)
+    >>> io_registry.register_writer("json", Cosmology, write_json)
+    >>> io_registry.register_identifier("json", Cosmology, json_identify)
+
+Now the registered functions can be used in
+:meth:`astropy.cosmology.Cosmology.read` and
+:meth:`astropy.cosmology.Cosmology.write`.

+.. doctest-skip:: win32
+
+    >>> import tempfile
+    >>> from astropy.cosmology import Planck18
+    >>>
+    >>> file = tempfile.NamedTemporaryFile()
+    >>> Planck18.write(file.name, format="json", overwrite=True)
+    >>> with open(file.name) as f: f.readlines()
+    ['{"cosmology": "FlatLambdaCDM", "name": "Planck18",
+       "H0": {"value": 67.66, "unit": "km / (Mpc s)"}, "Om0": 0.30966,
+       ...
+    >>>
+    >>> cosmo = Cosmology.read(file.name, format="json")
+    >>> file.close()
+    >>> cosmo == Planck18  # test it round-trips
+    True
+
+
+.. doctest::
+    :hide:
+
+    >>> io_registry.unregister_reader("json", Cosmology)
+    >>> io_registry.unregister_writer("json", Cosmology)
+    >>> io_registry.unregister_identifier("json", Cosmology)
+    >>> try:
+    ...     io_registry.get_reader("json", Cosmology)
+    ... except io_registry.IORegistryError:
+    ...     pass
+
+.. EXAMPLE END

 Reference/API
 =============

 .. automodapi:: astropy.cosmology.connect
+
+.. automodapi:: astropy.cosmology.io.mapping

diff --git a/docs/whatsnew/5.0.rst b/docs/whatsnew/5.0.rst
index b8c135bb606f..a224b149c972 100644
--- a/docs/whatsnew/5.0.rst
+++ b/docs/whatsnew/5.0.rst
@@ -9,6 +9,47 @@ What's New in Astropy 5.0?
 Overview
 ========

+.. _whatsnew-5.0-cosmology:
+
+Support for reading, writing, and converting ``Cosmology``
+==========================================================
+
+Four new methods -- ``read``, ``write``, ``to_format``, ``from_format`` -- have
+been added to the ``Cosmology`` class, enabling reading from and writing to
+files and converting between different python objects.
+The methods use Astropy's Unified I/O registry so custom formats can be
+registered. Details are discussed in an addition to the docs.
+
+Currently no file formats are registered, but the syntax is as follows:
+
+.. doctest-skip::
+
+    >>> from astropy.cosmology import Planck18
+    >>> Planck18.write('<file name>.<format>')
+    >>>
+    >>> from astropy.cosmology import Cosmology
+    >>> cosmo = Cosmology.read('<file name>.<format>')
+    >>> cosmo == Planck18
+    True
+
+
+The transformation between ``Cosmology`` and `dict` is pre-registered,
+e.g. enabling::
+
+    >>> from astropy.cosmology import Planck18
+    >>> cm = Planck18.to_format("mapping")
+    >>> cm
+    {'cosmology': <class 'astropy.cosmology.core.FlatLambdaCDM'>,
+     'name': 'Planck18',
+     'H0': <Quantity 67.66 km / (Mpc s)>,
+     'Om0': 0.30966,
+     ...
+
+    >>> from astropy.cosmology import Cosmology
+    >>> cosmo = Cosmology.from_format(cm, format="mapping")
+    >>> cosmo == Planck18
+    True
+

 Full change log
 ===============

If completing this task requires creating new files, classes, fields, or error messages, you may consider using the following suggested entity names:
[{'type': 'field', 'name': '_init_signature'}, {'type': 'file', 'name': 'astropy/cosmology/io/mapping.py'}, {'type': 'file', 'name': 'astropy/cosmology/io/__init__.py'}, {'type': 'field', 'name': 'kind'}, {'type': 'field', 'name': 'tuple'}, {'type': 'field', 'name': '_COSMOLOGY_CLASSES'}, {'type': 'field', 'name': 'TypeError'}, {'type': 'field', 'name': '__class__'}]