diff --git a/analyzer/CMakeLists.txt b/analyzer/CMakeLists.txt
index 63bff24a19ae185e6395562b42aca2a0ad89a568..236ae878d4230f0215124bddebe298fd94d71807 100644
--- a/analyzer/CMakeLists.txt
+++ b/analyzer/CMakeLists.txt
@@ -29,6 +29,7 @@ set(analyzer_lib_srcs
 add_library(UserFeedbackAnalyzer STATIC ${analyzer_lib_srcs})
 target_link_libraries(UserFeedbackAnalyzer Qt5::Network)
 target_include_directories(UserFeedbackAnalyzer PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR};>")
+target_compile_features(UserFeedbackAnalyzer PRIVATE cxx_generic_lambdas)
 
 set(analyzer_srcs
     aggregateddatamodel.cpp
diff --git a/analyzer/core/aggregation.cpp b/analyzer/core/aggregation.cpp
index bb4b3b680a1db53baa5b1045ad19ecf1a6e893db..6769fb0d552f0d304aa23304328dc073cb6c8039 100644
--- a/analyzer/core/aggregation.cpp
+++ b/analyzer/core/aggregation.cpp
@@ -16,9 +16,24 @@
 */
 
 #include "aggregation.h"
+#include "util.h"
+
+#include <QJsonArray>
+#include <QJsonObject>
 
 using namespace UserFeedback::Analyzer;
 
+static const struct {
+    Aggregation::Type type;
+    const char *name;
+} aggregation_types_table[] {
+    { Aggregation::None, "none" },
+    { Aggregation::Category, "category" },
+    { Aggregation::RatioSet, "ratio_set" },
+    { Aggregation::Numeric, "numeric" },
+    { Aggregation::XY, "xy" }
+};
+
 Aggregation::Aggregation() = default;
 Aggregation::~Aggregation() = default;
 
@@ -41,3 +56,31 @@ void Aggregation::setElements(const QVector<AggregationElement>& elements)
 {
     m_elements = elements;
 }
+
+QJsonObject Aggregation::toJsonObject() const
+{
+    QJsonObject obj;
+    obj.insert(QStringLiteral("type"), QLatin1String(aggregation_types_table[m_type].name));
+    QJsonArray elems;
+    for (const auto &e : m_elements)
+        elems.push_back(e.toJsonObject());
+    obj.insert(QStringLiteral("elements"), elems);
+    return obj;
+}
+
+QVector<Aggregation> Aggregation::fromJson(const Product &product, const QJsonArray& a)
+{
+    QVector<Aggregation> aggrs;
+    aggrs.reserve(a.size());
+    for (const auto &v : a) {
+        if (!v.isObject())
+            continue;
+        const auto obj = v.toObject();
+
+        Aggregation aggr;
+        aggr.setType(Util::stringToEnum<Aggregation::Type>(obj.value(QLatin1String("type")).toString(), aggregation_types_table));
+        aggr.setElements(AggregationElement::fromJson(product, obj.value(QLatin1String("elements")).toArray()));
+        aggrs.push_back(aggr);
+    }
+    return aggrs;
+}
diff --git a/analyzer/core/aggregation.h b/analyzer/core/aggregation.h
index b2332a594de90f63b93ce8f06f32a6e3941f62b6..61f6fef0833c0d48bfc88bce97bc03ec57f9d309 100644
--- a/analyzer/core/aggregation.h
+++ b/analyzer/core/aggregation.h
@@ -22,9 +22,14 @@
 
 #include <QTypeInfo>
 
+class QJsonArray;
+class QJsonObject;
+
 namespace UserFeedback {
 namespace Analyzer {
 
+class Product;
+
 class Aggregation
 {
     Q_GADGET
@@ -47,6 +52,9 @@ public:
     QVector<AggregationElement> elements() const;
     void setElements(const QVector<AggregationElement> &elements);
 
+    QJsonObject toJsonObject() const;
+    static QVector<Aggregation> fromJson(const Product &product, const QJsonArray &a);
+
 private:
     Type m_type = None;
     QVector<AggregationElement> m_elements;
diff --git a/analyzer/core/aggregationelement.cpp b/analyzer/core/aggregationelement.cpp
index 88304bfa9b37ddc43c04c72f36bd01c57055fd37..c7f80c6c0252e980587cf033c1f363955bce2ab2 100644
--- a/analyzer/core/aggregationelement.cpp
+++ b/analyzer/core/aggregationelement.cpp
@@ -16,9 +16,22 @@
 */
 
 #include "aggregationelement.h"
+#include "product.h"
+#include "util.h"
+
+#include <QJsonArray>
+#include <QJsonObject>
 
 using namespace UserFeedback::Analyzer;
 
+static const struct {
+    AggregationElement::Type type;
+    const char *name;
+} aggregation_element_types_table[] {
+    { AggregationElement::Value, "value" },
+    { AggregationElement::Size, "size" }
+};
+
 AggregationElement::AggregationElement() = default;
 AggregationElement::~AggregationElement() = default;
 
@@ -76,3 +89,44 @@ bool AggregationElement::operator==(const AggregationElement &other) const
     }
     Q_UNREACHABLE();
 }
+
+QJsonObject AggregationElement::toJsonObject() const
+{
+    QJsonObject obj;
+    obj.insert(QStringLiteral("type"), QLatin1String(aggregation_element_types_table[m_type].name));
+    switch (m_type) {
+        case Value:
+            obj.insert(QStringLiteral("schemaEntry"), m_entry.name());
+            obj.insert(QStringLiteral("schemaEntryElement"), m_element.name());
+            break;
+        case Size:
+            obj.insert(QStringLiteral("schemaEntry"), m_entry.name());
+            break;
+    }
+    return obj;
+}
+
+QVector<AggregationElement> AggregationElement::fromJson(const Product &product, const QJsonArray& a)
+{
+    QVector<AggregationElement> elems;
+    elems.reserve(a.size());
+    for (const auto &v : a) {
+        if (!v.isObject())
+            continue;
+        const auto obj = v.toObject();
+
+        AggregationElement e;
+        e.setType(Util::stringToEnum<AggregationElement::Type>(obj.value(QLatin1String("type")).toString(), aggregation_element_types_table));
+        switch (e.type()) {
+            case Value:
+                e.setSchemaEntry(product.schemaEntry(obj.value(QLatin1String("schemaEntry")).toString()));
+                e.setSchemaEntryElement(e.schemaEntry().element(obj.value(QLatin1String("schemaEntryElement")).toString()));
+                break;
+            case Size:
+                e.setSchemaEntry(product.schemaEntry(obj.value(QLatin1String("schemaEntry")).toString()));
+                break;
+        }
+        elems.push_back(e);
+    }
+    return elems;
+}
diff --git a/analyzer/core/aggregationelement.h b/analyzer/core/aggregationelement.h
index f037d10416a3ec8ce1fe6de0df15a7621d5b17b5..8889ad8d3e9937f8fc51d3517f41490fc6410e0c 100644
--- a/analyzer/core/aggregationelement.h
+++ b/analyzer/core/aggregationelement.h
@@ -21,9 +21,14 @@
 #include "schemaentry.h"
 #include "schemaentryelement.h"
 
+class QJsonArray;
+class QJsonObject;
+
 namespace UserFeedback {
 namespace Analyzer {
 
+class Product;
+
 class AggregationElement
 {
 public:
@@ -47,6 +52,9 @@ public:
 
     QString displayString() const;
 
+    QJsonObject toJsonObject() const;
+    static QVector<AggregationElement> fromJson(const Product &product, const QJsonArray &a);
+
 private:
     SchemaEntry m_entry;
     SchemaEntryElement m_element;
diff --git a/analyzer/core/product.cpp b/analyzer/core/product.cpp
index d3fae729a65e1f78d68c12c9fcb9d8e57dcceb08..3f72130b7d2d181b89cb635aad02519bd8239e2c 100644
--- a/analyzer/core/product.cpp
+++ b/analyzer/core/product.cpp
@@ -64,6 +64,16 @@ QVector<SchemaEntry> Product::schema() const
     return d->schema;
 }
 
+SchemaEntry Product::schemaEntry(const QString& name) const
+{
+    const auto it = std::find_if(d->schema.cbegin(), d->schema.cend(), [name](const auto &entry) {
+        return entry.name() == name;
+    });
+    if (it == d->schema.cend())
+        return {};
+    return *it;
+}
+
 void Product::setSchema(const QVector<SchemaEntry> &schema)
 {
     d->schema = schema;
@@ -83,10 +93,19 @@ QByteArray Product::toJson() const
 {
     QJsonObject obj;
     obj.insert(QStringLiteral("name"), name());
-    QJsonArray schema;
-    foreach (const auto &s, d->schema)
-        schema.push_back(s.toJsonObject());
-    obj.insert(QStringLiteral("schema"), schema);
+    {
+        QJsonArray schema;
+        foreach (const auto &s, d->schema)
+            schema.push_back(s.toJsonObject());
+        obj.insert(QStringLiteral("schema"), schema);
+    }
+
+    {
+        QJsonArray aggrs;
+        foreach (const auto &a, d->aggregations)
+            aggrs.push_back(a.toJsonObject());
+        obj.insert(QStringLiteral("aggregation"), aggrs);
+    }
     QJsonDocument doc(obj);
     return doc.toJson();
 }
@@ -96,21 +115,24 @@ static Product productFromJsonObject(const QJsonObject &obj)
     Product product;
     product.setName(obj.value(QStringLiteral("name")).toString());
     product.setSchema(SchemaEntry::fromJson(obj.value(QStringLiteral("schema")).toArray()));
+    product.setAggregations(Aggregation::fromJson(product, obj.value(QLatin1String("aggregation")).toArray()));
 
     // ### temporary HACK
-    QVector<Aggregation> aggrs;
-    for (const auto &entry : product.schema()) {
-        for (const auto &elem : entry.elements()) {
-            Aggregation aggr;
-            aggr.setType(Aggregation::Category);
-            AggregationElement e;
-            e.setSchemaEntry(entry);
-            e.setSchemaEntryElement(elem);
-            aggr.setElements({e});
-            aggrs.push_back(aggr);
+    if (product.aggregations().isEmpty()) {
+        QVector<Aggregation> aggrs;
+        for (const auto &entry : product.schema()) {
+            for (const auto &elem : entry.elements()) {
+                Aggregation aggr;
+                aggr.setType(Aggregation::Category);
+                AggregationElement e;
+                e.setSchemaEntry(entry);
+                e.setSchemaEntryElement(elem);
+                aggr.setElements({e});
+                aggrs.push_back(aggr);
+            }
         }
+        product.setAggregations(aggrs);
     }
-    product.setAggregations(aggrs);
 
     return product;
 }
diff --git a/analyzer/core/product.h b/analyzer/core/product.h
index 209cd46003c26a149fad12ceb687647f6deb947a..be76595f43cd72c20d7ed443ab97d4d0e5d3342c 100644
--- a/analyzer/core/product.h
+++ b/analyzer/core/product.h
@@ -48,6 +48,7 @@ public:
 
     QVector<SchemaEntry> schema() const;
     void setSchema(const QVector<SchemaEntry>& schema);
+    SchemaEntry schemaEntry(const QString &name) const;
 
     QVector<Aggregation> aggregations() const;
     void setAggregations(const QVector<Aggregation> &aggregations);
diff --git a/analyzer/core/schemaentry.cpp b/analyzer/core/schemaentry.cpp
index 9c00e8091047f56fe7a907654f72b7eb615ed0d7..bb1916cd250046b1c82f47e62e35968383087288 100644
--- a/analyzer/core/schemaentry.cpp
+++ b/analyzer/core/schemaentry.cpp
@@ -127,6 +127,16 @@ void SchemaEntry::setElements(const QVector<SchemaEntryElement> &elements)
     d->elements = elements;
 }
 
+SchemaEntryElement SchemaEntry::element(const QString& name) const
+{
+    const auto it = std::find_if(d->elements.cbegin(), d->elements.cend(), [name](const auto &entry) {
+        return entry.name() == name;
+    });
+    if (it == d->elements.cend())
+        return {};
+    return *it;
+}
+
 QJsonObject SchemaEntry::toJsonObject() const
 {
     QJsonObject obj;
diff --git a/analyzer/core/schemaentry.h b/analyzer/core/schemaentry.h
index d01b4a445d378d0ce734bbb53b2e71072d589da2..80917b2f97f793c0e91b5c6c5a8984aad5419ad8 100644
--- a/analyzer/core/schemaentry.h
+++ b/analyzer/core/schemaentry.h
@@ -96,6 +96,7 @@ public:
 
     QVector<SchemaEntryElement> elements() const;
     void setElements(const QVector<SchemaEntryElement> &elements);
+    SchemaEntryElement element(const QString &name) const;
 
     QJsonObject toJsonObject() const;
     static QVector<SchemaEntry> fromJson(const QJsonArray &array);
diff --git a/tests/auto/CMakeLists.txt b/tests/auto/CMakeLists.txt
index 8e4571a2b5d454c906292ef19e00b39aeb271ea6..edd27876a9d80947657b99a7cdc576ef8302d38f 100644
--- a/tests/auto/CMakeLists.txt
+++ b/tests/auto/CMakeLists.txt
@@ -12,6 +12,7 @@ function(uf_add_test _file)
     add_test(NAME ${_name} COMMAND ${_name})
 endfunction()
 
+uf_add_test(producttest UserFeedbackAnalyzer)
 uf_add_test(productapitest UserFeedbackTestUtils)
 uf_add_test(productmodeltest UserFeedbackTestUtils)
 uf_add_test(schemamodeltest UserFeedbackTestUtils)
diff --git a/tests/auto/producttest.cpp b/tests/auto/producttest.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..768dd5d97353799ee5b78f7aedf3e984c137d910
--- /dev/null
+++ b/tests/auto/producttest.cpp
@@ -0,0 +1,98 @@
+/*
+    Copyright (C) 2016 Volker Krause <vkrause@kde.org>
+
+    This program is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This program is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <analyzer/core/aggregation.h>
+#include <analyzer/core/product.h>
+
+#include <3rdparty/qt/modeltest.h>
+
+#include <QDebug>
+#include <QtTest/qtest.h>
+#include <QObject>
+#include <QStandardPaths>
+
+using namespace UserFeedback::Analyzer;
+
+class ProductTest : public QObject
+{
+    Q_OBJECT
+private slots:
+    void initTestCase()
+    {
+        Q_INIT_RESOURCE(schemaentrytemplates);
+        QStandardPaths::setTestModeEnabled(true);
+    }
+
+    void testFromJson()
+    {
+        const auto ps = Product::fromJson(R"({
+            "name": "org.kde.TestProduct",
+            "schema": [{
+                "name": "entry1",
+                "type": "scalar",
+                "elements": [{ "name": "elem11", "type": "string" }]
+            }, {
+                "name": "entry2",
+                "type": "list",
+                "elements": [{ "name": "elem21", "type": "number" }]
+            }],
+            "aggregation": [{
+                "type": "ratio_set",
+                "elements": [{ "type": "value", "schemaEntry": "entry1", "schemaEntryElement": "elem11" }]
+            }, {
+                "type": "numeric",
+                "elements": [{ "type": "size", "schemaEntry": "entry2" }]
+            }]
+        })");
+
+        QCOMPARE(ps.size(), 1);
+
+        const auto p = ps.at(0);
+        QCOMPARE(p.name(), QLatin1String("org.kde.TestProduct"));
+
+        QCOMPARE(p.schema().size(), 2);
+
+        const auto aggrs = p.aggregations();
+        QCOMPARE(aggrs.size(), 2);
+        {
+            const auto a1 = aggrs.at(0);
+            QCOMPARE(a1.type(), Aggregation::RatioSet);
+            const auto a1elems = a1.elements();
+            QCOMPARE(a1elems.size(), 1);
+            QCOMPARE(a1elems.at(0).type(), AggregationElement::Value);
+            QCOMPARE(a1elems.at(0).schemaEntry().name(), QLatin1String("entry1"));
+            QCOMPARE(a1elems.at(0).schemaEntry().dataType(), SchemaEntry::Scalar);
+            QCOMPARE(a1elems.at(0).schemaEntryElement().name(), QLatin1String("elem11"));
+        }
+
+        {
+            const auto a2 = aggrs.at(1);
+            QCOMPARE(a2.type(), Aggregation::Numeric);
+            const auto a2elems = a2.elements();
+            QCOMPARE(a2elems.size(), 1);
+            QCOMPARE(a2elems.at(0).type(), AggregationElement::Size);
+            QCOMPARE(a2elems.at(0).schemaEntry().name(), QLatin1String("entry2"));
+            QCOMPARE(a2elems.at(0).schemaEntry().dataType(), SchemaEntry::List);
+            QVERIFY(a2elems.at(0).schemaEntryElement().name().isEmpty());
+        }
+    }
+
+};
+
+QTEST_MAIN(ProductTest)
+
+#include "producttest.moc"