Index: src/sas/qtgui/MainWindow/DataExplorer.py
===================================================================
--- src/sas/qtgui/MainWindow/DataExplorer.py	(revision 3b95b3bf18e0282898b94748d7c29f60e7fb5b20)
+++ src/sas/qtgui/MainWindow/DataExplorer.py	(revision 345b3b382101552fe1f6febd4afefb152aa1765e)
@@ -233,43 +233,5 @@
         filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
         if filename:
-            load_thread = threads.deferToThread(self.readProject, filename)
-            load_thread.addCallback(self.readProjectComplete)
-            load_thread.addErrback(self.readProjectFailed)
-
-    def loadFailed(self, reason):
-        """
-        """
-        print("file load FAILED: ", reason)
-        pass
-
-    def readProjectFailed(self, reason):
-        """
-        """
-        print("readProjectFailed FAILED: ", reason)
-        pass
-
-    def readProject(self, filename):
-        self.communicator.statusBarUpdateSignal.emit("Loading Project... %s" % os.path.basename(filename))
-        try:
-            manager = DataManager()
-            with open(filename, 'r') as infile:
-                manager.load_from_readable(infile)
-
-            self.communicator.statusBarUpdateSignal.emit("Loaded Project: %s" % os.path.basename(filename))
-            return manager
-
-        except:
-            self.communicator.statusBarUpdateSignal.emit("Failed: %s" % os.path.basename(filename))
-            raise
-
-    def readProjectComplete(self, manager):
-        self.model.clear()
-
-        self.manager.assign(manager)
-        self.model.beginResetModel()
-        for id, item in self.manager.get_all_data().items():
-            self.updateModel(item.data, item.path)
-
-        self.model.endResetModel()
+            self.readProject(filename)
 
     def saveProject(self):
@@ -285,11 +247,94 @@
         name_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
         filename = name_tuple[0]
-        if filename:
-            _, extension = os.path.splitext(filename)
-            if not extension:
-                filename = '.'.join((filename, 'json'))
-            self.communicator.statusBarUpdateSignal.emit("Saving Project... %s\n" % os.path.basename(filename))
-            with open(filename, 'w') as outfile:
-                self.manager.save_to_writable(outfile)
+        if not filename:
+            return
+        _, extension = os.path.splitext(filename)
+        if not extension:
+            filename = '.'.join((filename, 'json'))
+        self.communicator.statusBarUpdateSignal.emit("Saving Project... %s\n" % os.path.basename(filename))
+
+        with open(filename, 'w') as outfile:
+            self.saveDataToFile(outfile)
+
+    def allDataForModel(self, model):
+        # data model
+        all_data = {}
+        for i in range(model.rowCount()):
+            properties = {}
+            item = model.item(i)
+            data = GuiUtils.dataFromItem(item)
+            if data is None: continue
+            # Now, all plots under this item
+            filename = data.filename
+            is_checked = item.checkState()
+            properties['checked'] = is_checked
+            other_datas = GuiUtils.plotsFromFilename(filename, model)
+            # skip the main plot
+            other_datas = list(other_datas.values())[1:]
+            all_data[data.id] = [data, properties, other_datas]
+        return all_data
+
+    def saveDataToFile(self, outfile):
+        """
+        Save every dataset to a json file
+        """
+        data = self.allDataForModel(self.model)
+        theory = self.allDataForModel(self.theory_model)
+
+        all_data = {}
+        for key, value in data.items():
+            all_data[key] = value
+        for key, value in theory.items():
+            if key in all_data:
+                raise ValueError("Inconsistent data in Project file.")
+            all_data[key] = value
+
+        # perspectives
+        current_perspective = self._perspective()
+        try:
+            perspective_dump = current_perspective.getCurrentStateAsXml()
+        except Exception:
+            ex = "State of " + current_perspective.windowTitle() + \
+                " cannot be saved."
+            logging.error(ex)
+        if perspective_dump is not None:
+            assert(isinstance(perspective_dump, dict))
+            all_data['perspective'] = perspective_dump
+        # save datas
+        GuiUtils.saveData(outfile, all_data)
+        return
+
+    def readProject(self, filename):
+        """
+        Read out datasets and fitpages from file
+        """
+        with open(filename, 'r') as infile:
+            all_data = GuiUtils.readDataFromFile(infile)
+        # clear the model
+        self.model.clear()
+
+        #self.model.beginResetModel()
+        for key, value in all_data.items():
+            # key - cardinal number of dataset
+            # value - main dataset, [dependant filesets]
+            # add the main index
+            new_data = value[0]
+            assert isinstance(new_data, (Data1D, Data2D))
+            properties = value[1]
+            is_checked = properties['checked']
+            new_item = GuiUtils.createModelItemWithPlot(new_data, new_data.filename)
+            new_item.setCheckState(is_checked)
+            model = self.theory_model
+            if new_data.is_data:
+                model = self.model
+            model.appendRow(new_item)
+            self.manager.add_data(data_list={new_data.id:new_data})
+
+            # Add the underlying data
+            if not value[2]:
+                continue
+            for plot in value[2]:
+                assert isinstance(plot, (Data1D, Data2D))
+                GuiUtils.updateModelItemWithPlot(new_item, plot, plot.name)
 
     def deleteFile(self, event):
@@ -1394,5 +1439,6 @@
         checkbox_item.setCheckable(True)
         checkbox_item.setCheckState(QtCore.Qt.Checked)
-        checkbox_item.setText(os.path.basename(p_file))
+        if p_file is not None:
+            checkbox_item.setText(os.path.basename(p_file))
 
         # Add the actual Data1D/Data2D object
Index: src/sas/qtgui/MainWindow/DataManager.py
===================================================================
--- src/sas/qtgui/MainWindow/DataManager.py	(revision e2e5f3d84e3cde792395c3cd184a5bba1eeb5873)
+++ src/sas/qtgui/MainWindow/DataManager.py	(revision 345b3b382101552fe1f6febd4afefb152aa1765e)
@@ -29,5 +29,4 @@
 from sas.qtgui.Plotting.PlotterData import Data1D
 from sas.qtgui.Plotting.PlotterData import Data2D
-from sas.qtgui.Plotting.Plottables import Plottable
 from sas.qtgui.Plotting.Plottables import PlottableTheory1D
 from sas.qtgui.Plotting.Plottables import PlottableFit1D
Index: src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py	(revision 20f4857d1e571c0d896afeff05f4b893b398ddc1)
+++ src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py	(revision 345b3b382101552fe1f6febd4afefb152aa1765e)
@@ -337,4 +337,13 @@
         pass
 
+    def getCurrentStateAsXml(self):
+        """
+        Returns an XML version of the current state
+        """
+        state = {}
+        for tab in self.tabs:
+            pass
+        return state
+
     @property
     def currentTab(self):
Index: src/sas/qtgui/Utilities/GuiUtils.py
===================================================================
--- src/sas/qtgui/Utilities/GuiUtils.py	(revision 63467b61b55727b3a1e6bd20f8e86e6b45006844)
+++ src/sas/qtgui/Utilities/GuiUtils.py	(revision 345b3b382101552fe1f6febd4afefb152aa1765e)
@@ -11,4 +11,6 @@
 import webbrowser
 import urllib.parse
+import json
+from io import BytesIO
 
 import numpy as np
@@ -26,4 +28,13 @@
 from sas.qtgui.Plotting.PlotterData import Data1D
 from sas.qtgui.Plotting.PlotterData import Data2D
+from sas.qtgui.Plotting.Plottables import Plottable
+from sas.sascalc.dataloader.data_info import Sample, Source, Vector
+from sas.qtgui.Plotting.Plottables import View
+from sas.qtgui.Plotting.Plottables import PlottableTheory1D
+from sas.qtgui.Plotting.Plottables import PlottableFit1D
+from sas.qtgui.Plotting.Plottables import Text
+from sas.qtgui.Plotting.Plottables import Chisq
+from sas.qtgui.MainWindow.DataState import DataState
+
 from sas.sascalc.dataloader.loader import Loader
 from sas.qtgui.Utilities import CustomDir
@@ -286,5 +297,5 @@
     changeDataExplorerTabSignal = QtCore.pyqtSignal(int)
 
-def updateModelItemWithPlot(item, update_data, name=""):
+def updateModelItemWithPlot(item, update_data, name="", checkbox_state=None):
     """
     Adds a checkboxed row named "name" to QStandardItem
@@ -311,8 +322,9 @@
             # Force redisplay
             return
-
     # Create the new item
     checkbox_item = createModelItemWithPlot(update_data, name)
 
+    if checkbox_state is not None:
+        checkbox_item.setCheckState(checkbox_state)
     # Append the new row to the main item
     item.appendRow(checkbox_item)
@@ -1139,4 +1151,141 @@
     return result
 
+def saveData(fp, data):
+    """
+    save content of data to fp (a .write()-supporting file-like object)
+    """
+
+    def add_type(dict, type):
+        dict['__type__'] = type.__name__
+        return dict
+
+    def jdefault(o):
+        """
+        objects that can't otherwise be serialized need to be converted
+        """
+        # tuples and sets (TODO: default JSONEncoder converts tuples to lists, create custom Encoder that preserves tuples)
+        if isinstance(o, (tuple, set)):
+            content = { 'data': list(o) }
+            return add_type(content, type(o))
+
+        # "simple" types
+        if isinstance(o, (Sample, Source, Vector)):
+            return add_type(o.__dict__, type(o))
+        if isinstance(o, (Plottable, View)):
+            return add_type(o.__dict__, type(o))
+
+        # DataState
+        if isinstance(o, (Data1D, Data2D)):
+            # don't store parent
+            content = o.__dict__.copy()
+            #content.pop('parent')
+            return add_type(content, type(o))
+
+        # ndarray
+        if isinstance(o, np.ndarray):
+            buffer = BytesIO()
+            np.save(buffer, o)
+            buffer.seek(0)
+            content = { 'data': buffer.read().decode('latin-1') }
+            return add_type(content, type(o))
+
+        # not supported
+        logging.info("data cannot be serialized to json: %s" % type(o))
+        return None
+
+    json.dump(data, fp, indent=2, sort_keys=True, default=jdefault)
+
+def readDataFromFile(fp):
+    '''
+    '''
+    supported = [
+        tuple, set,
+        Sample, Source, Vector,
+        Plottable, Data1D, Data2D, PlottableTheory1D, PlottableFit1D, Text, Chisq, View,
+        DataState, np.ndarray]
+
+    lookup = dict((cls.__name__, cls) for cls in supported)
+
+    class TooComplexException(Exception):
+        pass
+
+    def simple_type(cls, data, level):
+        class Empty(object):
+            def __init__(self):
+                for key, value in data.items():
+                    setattr(self, key, generate(value, level))
+
+        # create target object
+        o = Empty()
+        o.__class__ = cls
+
+        return o
+
+    def construct(type, data, level):
+        try:
+            cls = lookup[type]
+        except KeyError:
+            logging.info('unknown type: %s' % type)
+            return None
+
+        # tuples and sets
+        if cls in (tuple, set):
+            # convert list to tuple/set
+            return cls(generate(data['data'], level))
+
+        # "simple" types
+        if cls in (Sample, Source, Vector):
+            return simple_type(cls, data, level)
+        if issubclass(cls, Plottable) or (cls == View):
+            return simple_type(cls, data, level)
+
+        # DataState
+        if cls == DataState:
+            o = simple_type(cls, data, level)
+            o.parent = None # TODO: set to ???
+            return o
+
+        # ndarray
+        if cls == np.ndarray:
+            buffer = BytesIO()
+            buffer.write(data['data'].encode('latin-1'))
+            buffer.seek(0)
+            return np.load(buffer)
+
+        logging.info('not implemented: %s, %s' % (type, cls))
+        return None
+
+    def generate(data, level):
+        if level > 16: # recursion limit (arbitrary number)
+            raise TooComplexException()
+        else:
+            level += 1
+
+        if isinstance(data, dict):
+            try:
+                type = data['__type__']
+            except KeyError:
+                # if dictionary doesn't have __type__ then it is assumed to be just an ordinary dictionary
+                o = {}
+                for key, value in data.items():
+                    o[key] = generate(value, level)
+                return o
+
+            return construct(type, data, level)
+
+        if isinstance(data, list):
+            return [generate(item, level) for item in data]
+
+        return data
+
+    new_stored_data = {}
+    for id, data in json.load(fp).items():
+        try:
+            new_stored_data[id] = generate(data, 0)
+        except TooComplexException:
+            logging.info('unable to load %s' % id)
+
+    return new_stored_data
+
 
 def enum(*sequential, **named):
