diff --git a/.gitignore b/.gitignore
index a5b7d04336a4a3dd2c22ab2e8f7c2522d73f20c3..d84ffeed9a4cc08ae800a42ddd2d9d446b11e678 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-*.jarsrc/test/java/org/jastadd/relast/tests/Resolver2.java
+*.jar
 .idea/
 .gradle/
 build
@@ -8,6 +8,8 @@ out/
 *.class
 src/test/jastadd/relations/Relations.ast
 src/test/jastadd/relations/Relations.jadd
+src/test/jastadd/relations/Relations2.ast
+src/test/jastadd/relations/Relations2.jadd
 src/test/jastadd/lowerbounds/LowerBounds.ast
 src/test/jastadd/lowerbounds/LowerBounds.jadd
 src/test/jastadd/multiple/Multiple.ast
@@ -15,8 +17,14 @@ src/test/jastadd/multiple/Multiple.jadd
 src/test/jastadd/resolver/Resolver.ast
 src/test/jastadd/resolver/Resolver.jadd
 src/test/jastadd/resolver/ResolverRefResolver.jadd
+src/test/jastadd/resolver/ResolverResolverStubs.jrag
 src/test/jastadd/resolver2/Resolver.ast
 src/test/jastadd/resolver2/Resolver.jadd
 src/test/jastadd/resolver2/ResolverRefResolver.jadd
+src/test/jastadd/resolver2/ResolverResolverStubs.jrag
 src/test/jastadd/listnames/ListNames.ast
-src/test/jastadd/listnames/ListNames.jadd
\ No newline at end of file
+src/test/jastadd/listnames/ListNames.jadd
+src/test/jastadd/serializer/Serializer.ast
+src/test/jastadd/serializer/Serializer.jadd
+src/test/jastadd/serializer/SerializerSerializer.jadd
+src/test/jastadd/serializer/SerializerRefResolver.jadd
diff --git a/build.gradle b/build.gradle
index 0383ec1426d52eb248d733b7fd55a1d748f8a07b..be8ffdec62de203a3abb621cbb96a04ce61f7b36 100644
--- a/build.gradle
+++ b/build.gradle
@@ -21,6 +21,8 @@ buildscript {
 dependencies {
     testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.0'
     testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.0'
+    compile 'com.fasterxml.jackson.core:jackson-core:2.9.8'
+    compile 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
     runtime 'org.jastadd:jastadd:2.3.2'
     compile group: 'net.sf.beaver', name: 'beaver-rt', version: '0.9.11'
     compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.10.0'
@@ -49,6 +51,7 @@ jar {
 jastadd {
     configureModuleBuild()
     modules {
+        //noinspection GroovyAssignabilityCheck
         module("RelAst") {
 
             java {
@@ -110,6 +113,7 @@ task preprocessRelationTest(type: JavaExec, group: 'verification') {
 
     classpath = sourceSets.main.runtimeClasspath
     main = 'org.jastadd.relast.compiler.Compiler'
+    //noinspection GroovyAssignabilityCheck
     args 'src/test/jastadd/relations/Relations.relast', '--file', '--grammarName=src/test/jastadd/relations/Relations'
 }
 
@@ -121,6 +125,7 @@ task doublePreprocessRelationTest(type: JavaExec, group: 'verification') {
 
     classpath = sourceSets.main.runtimeClasspath
     main = 'org.jastadd.relast.compiler.Compiler'
+    //noinspection GroovyAssignabilityCheck
     args 'src/test/jastadd/relations/Relations.ast', '--file', '--grammarName=src/test/jastadd/relations/Relations2'
 }
 
@@ -132,7 +137,11 @@ task compileRelationTest(type: JavaExec, group: 'verification') {
 
     classpath = sourceSets.main.runtimeClasspath
     main = 'org.jastadd.JastAdd'
-    args '--o=src/test/java-gen/', '--package=relations.ast', 'src/test/jastadd/relations/Relations.ast', 'src/test/jastadd/relations/Relations.jadd', 'src/test/jastadd/Utils.jadd'
+    //noinspection GroovyAssignabilityCheck
+    args '--o=src/test/java-gen/', '--package=relations.ast',
+            'src/test/jastadd/relations/Relations.ast',
+            'src/test/jastadd/relations/Relations.jadd',
+            'src/test/jastadd/Utils.jadd'
 }
 
 task preprocessLowerBoundsTest(type: JavaExec, group: 'verification') {
@@ -143,6 +152,7 @@ task preprocessLowerBoundsTest(type: JavaExec, group: 'verification') {
 
     classpath = sourceSets.main.runtimeClasspath
     main = 'org.jastadd.relast.compiler.Compiler'
+    //noinspection GroovyAssignabilityCheck
     args 'src/test/jastadd/lowerbounds/LowerBounds.relast', '--file', '--grammarName=src/test/jastadd/lowerbounds/LowerBounds'
 }
 
@@ -154,7 +164,11 @@ task compileLowerBoundsTest(type: JavaExec, group: 'verification') {
 
     classpath = sourceSets.main.runtimeClasspath
     main = 'org.jastadd.JastAdd'
-    args '--o=src/test/java-gen/', '--package=lowerbounds.ast', 'src/test/jastadd/lowerbounds/LowerBounds.ast', 'src/test/jastadd/lowerbounds/LowerBounds.jadd', 'src/test/jastadd/Utils.jadd'
+    //noinspection GroovyAssignabilityCheck
+    args '--o=src/test/java-gen/', '--package=lowerbounds.ast',
+            'src/test/jastadd/lowerbounds/LowerBounds.ast',
+            'src/test/jastadd/lowerbounds/LowerBounds.jadd',
+            'src/test/jastadd/Utils.jadd'
 }
 
 task preprocessMultipleTest(type: JavaExec, group: 'verification') {
@@ -165,6 +179,7 @@ task preprocessMultipleTest(type: JavaExec, group: 'verification') {
 
     classpath = sourceSets.main.runtimeClasspath
     main = 'org.jastadd.relast.compiler.Compiler'
+    //noinspection GroovyAssignabilityCheck
     args 'src/test/jastadd/multiple/Part1.relast', 'src/test/jastadd/multiple/Part2.relast', 'src/test/jastadd/multiple/Part3.relast', '--file', '--grammarName=src/test/jastadd/multiple/Multiple'
 }
 
@@ -176,17 +191,22 @@ task compileMultipleTest(type: JavaExec, group: 'verification') {
 
     classpath = sourceSets.main.runtimeClasspath
     main = 'org.jastadd.JastAdd'
-    args '--o=src/test/java-gen/', '--package=multiple.ast', 'src/test/jastadd/multiple/Multiple.ast', 'src/test/jastadd/multiple/Multiple.jadd', 'src/test/jastadd/Utils.jadd'
+    //noinspection GroovyAssignabilityCheck
+    args '--o=src/test/java-gen/', '--package=multiple.ast',
+            'src/test/jastadd/multiple/Multiple.ast',
+            'src/test/jastadd/multiple/Multiple.jadd',
+            'src/test/jastadd/Utils.jadd'
 }
 
 task preprocessResolverTest(type: JavaExec, group: 'verification') {
 
     doFirst {
-        delete 'src/test/jastadd/resolver/Resolver.ast', 'src/test/jastadd/resolver/Resolver.jadd', 'src/test/jastadd/resolver/ResolverRefResolver.jadd'
+        delete 'src/test/jastadd/resolver/Resolver.ast', 'src/test/jastadd/resolver/Resolver.jadd', 'src/test/jastadd/resolver/ResolverRefResolver.jadd', 'src/test/jastadd/resolver/ResolverResolverStubs.jrag'
     }
 
     classpath = sourceSets.main.runtimeClasspath
     main = 'org.jastadd.relast.compiler.Compiler'
+    //noinspection GroovyAssignabilityCheck
     args 'src/test/jastadd/resolver/Resolver.relast', '--file', '--grammarName=src/test/jastadd/resolver/Resolver', '--resolverHelper'
 }
 
@@ -198,7 +218,15 @@ task compileResolverTest(type: JavaExec, group: 'verification') {
 
     classpath = sourceSets.main.runtimeClasspath
     main = 'org.jastadd.JastAdd'
-    args '--o=src/test/java-gen/', '--package=resolver.ast', 'src/test/jastadd/resolver/Resolver.ast', 'src/test/jastadd/resolver/Resolver.jadd', 'src/test/jastadd/resolver/ResolverUtils.jadd', 'src/test/jastadd/resolver/ResolverRefResolver.jadd', 'src/test/jastadd/resolver/MyRefResolver.jadd', 'src/test/jastadd/Utils.jadd'
+    //noinspection GroovyAssignabilityCheck
+    args '--o=src/test/java-gen/', '--package=resolver.ast',
+            'src/test/jastadd/resolver/Resolver.ast',
+            'src/test/jastadd/resolver/Resolver.jadd',
+            'src/test/jastadd/resolver/ResolverUtils.jadd',
+            'src/test/jastadd/resolver/ResolverRefResolver.jadd',
+            'src/test/jastadd/resolver/ResolverResolverStubs.jrag',
+            'src/test/jastadd/resolver/MyRefResolver.jadd',
+            'src/test/jastadd/Utils.jadd'
 }
 
 
@@ -210,6 +238,7 @@ task preprocessResolver2Test(type: JavaExec, group: 'verification') {
 
     classpath = sourceSets.main.runtimeClasspath
     main = 'org.jastadd.relast.compiler.Compiler'
+    //noinspection GroovyAssignabilityCheck
     args 'src/test/jastadd/resolver2/Resolver.relast', '--file', '--grammarName=src/test/jastadd/resolver2/Resolver', '--resolverHelper'
 }
 
@@ -221,7 +250,15 @@ task compileResolver2Test(type: JavaExec, group: 'verification') {
 
     classpath = sourceSets.main.runtimeClasspath
     main = 'org.jastadd.JastAdd'
-    args '--o=src/test/java-gen/', '--package=resolver2.ast', 'src/test/jastadd/resolver2/Resolver.ast', 'src/test/jastadd/resolver2/Resolver.jadd', 'src/test/jastadd/resolver2/ResolverUtils.jadd', 'src/test/jastadd/resolver2/ResolverRefResolver.jadd', 'src/test/jastadd/resolver2/MyRefResolver.jadd', 'src/test/jastadd/Utils.jadd'
+    //noinspection GroovyAssignabilityCheck
+    args '--o=src/test/java-gen/', '--package=resolver2.ast',
+            'src/test/jastadd/resolver2/Resolver.ast',
+            'src/test/jastadd/resolver2/Resolver.jadd',
+            'src/test/jastadd/resolver2/ResolverUtils.jadd',
+            'src/test/jastadd/resolver2/ResolverRefResolver.jadd',
+            'src/test/jastadd/resolver2/ResolverResolverStubs.jrag',
+            'src/test/jastadd/resolver2/MyRefResolver.jadd',
+            'src/test/jastadd/Utils.jadd'
 }
 
 task preprocessListNamesTest(type: JavaExec, group: 'verification') {
@@ -232,6 +269,7 @@ task preprocessListNamesTest(type: JavaExec, group: 'verification') {
 
     classpath = sourceSets.main.runtimeClasspath
     main = 'org.jastadd.relast.compiler.Compiler'
+    //noinspection GroovyAssignabilityCheck
     args 'src/test/jastadd/listnames/ListNames.relast', '--file', '--grammarName=src/test/jastadd/listnames/ListNames', '--jastAddList=ListyMcListface'
 }
 
@@ -243,9 +281,44 @@ task compileListNamesTest(type: JavaExec, group: 'verification') {
 
     classpath = sourceSets.main.runtimeClasspath
     main = 'org.jastadd.JastAdd'
-    args '--o=src/test/java-gen/', '--package=listnames.ast', 'src/test/jastadd/listnames/ListNames.ast', 'src/test/jastadd/listnames/ListNames.jadd', 'src/test/jastadd/Utils.jadd', '--List=ListyMcListface'
+    //noinspection GroovyAssignabilityCheck
+    args '--o=src/test/java-gen/', '--package=listnames.ast', '--List=ListyMcListface',
+            'src/test/jastadd/listnames/ListNames.ast',
+            'src/test/jastadd/listnames/ListNames.jadd',
+            'src/test/jastadd/Utils.jadd'
 }
 
+task preprocessSerializerTest(type: JavaExec, group: 'verification') {
+
+    doFirst {
+        delete 'src/test/jastadd/serializer/Serializer.ast',
+                'src/test/jastadd/serializer/Serializer.jadd',
+                'src/test/jastadd/serializer/SerializerRefResolver.jadd',
+                'src/test/jastadd/serializer/SerializerSerializer.jadd'
+    }
+
+    classpath = sourceSets.main.runtimeClasspath
+    main = 'org.jastadd.relast.compiler.Compiler'
+    //noinspection GroovyAssignabilityCheck
+    args 'src/test/jastadd/serializer/Serializer.relast', '--file', '--grammarName=src/test/jastadd/serializer/Serializer', '--serializer=jackson'
+}
+
+task compileSerializerTest(type: JavaExec, group: 'verification') {
+
+    doFirst {
+        delete 'src/test/java-gen/serializer'
+    }
+
+    classpath = sourceSets.main.runtimeClasspath
+    main = 'org.jastadd.JastAdd'
+    //noinspection GroovyAssignabilityCheck
+    args '--o=src/test/java-gen/', '--package=serializer.ast',
+            'src/test/jastadd/serializer/Serializer.ast',
+            'src/test/jastadd/serializer/Serializer.jadd',
+            'src/test/jastadd/serializer/SerializerRefResolver.jadd',
+            'src/test/jastadd/serializer/SerializerSerializer.jadd',
+            'src/test/jastadd/Utils.jadd'
+}
 
 test {
     outputs.upToDateWhen { false }
@@ -270,4 +343,7 @@ test.dependsOn compileResolver2Test
 compileResolver2Test.dependsOn preprocessResolver2Test
 
 test.dependsOn compileListNamesTest
-compileListNamesTest.dependsOn preprocessListNamesTest
\ No newline at end of file
+compileListNamesTest.dependsOn preprocessListNamesTest
+
+test.dependsOn compileSerializerTest
+compileSerializerTest.dependsOn preprocessSerializerTest
\ No newline at end of file
diff --git a/src/main/jastadd/Backend.jadd b/src/main/jastadd/Backend.jadd
index b079db04c539f64711d790bfcebd2ecc46076215..c33893c3aec0ba7c50e507c6d544016880560779 100644
--- a/src/main/jastadd/Backend.jadd
+++ b/src/main/jastadd/Backend.jadd
@@ -3,7 +3,8 @@ aspect BackendAbstractGrammar {
   public static String ASTNode.listClass = "ArrayList";
   public static String ASTNode.jastAddListType = "List";
 
-public static boolean ASTNode.resolverHelper = false;
+  public static boolean ASTNode.resolverHelper = false;
+  public static boolean ASTNode.serializer = false;
 
   public String Program.generateAbstractGrammar() {
     StringBuilder sb = new StringBuilder();
@@ -694,7 +695,13 @@ aspect NameResolutionHelper {
     return sb.toString();
   }
 
-  public void Program.generateRewriteToSuperTypeStub(StringBuilder sb) {
+  public String Program.generateResolverStubs() {
+    StringBuilder sb = new StringBuilder();
+    generateResolverStubs(sb);
+    return sb.toString();
+  }
+
+  public void Program.generateResolverStubs(StringBuilder sb) {
     sb.append("aspect RefResolverStubs {\n\n");
 
     for (Relation r: getRelations()) {
@@ -708,6 +715,9 @@ aspect NameResolutionHelper {
       }
     }
     sb.append("}\n\n");
+  }
+
+  public void Program.generateRewriteToSuperTypeStub(StringBuilder sb) {
 
     sb.append("aspect ReferenceCreation {\n\n");
 
@@ -871,6 +881,391 @@ aspect NameResolutionHelper {
   }
 }
 
+aspect Serializer {
+
+  protected static String ASTNode.jsonTypeKey = "type";
+  protected static String ASTNode.jsonNodeType = "com.fasterxml.jackson.databind.JsonNode";
+  protected static String ASTNode.jsonNodeTypeAccessor = ".get(\"" + jsonTypeKey + "\").asText()";
+  public String Program.generateSerializer() {
+    StringBuilder sb = new StringBuilder();
+    generateFromJson(sb);
+    generateToJson(sb);
+    writeUID(sb);
+    return sb.toString();
+  }
+
+  public void Program.generateFromJson(StringBuilder sb) {
+    sb.append("aspect JsonToModel {\n");
+
+    sb.append(ind(1) + "public class DeserializationException extends Exception {\n");
+    sb.append(ind(2) + "public DeserializationException(String message) {\n");
+    sb.append(ind(3) + "super(message);\n");
+    sb.append(ind(2) + "}\n");
+    sb.append(ind(2) + "public DeserializationException(String message, Exception cause) {\n");
+    sb.append(ind(3) + "super(message, cause);\n");
+    sb.append(ind(2) + "}\n");
+    sb.append(ind(1) + "}\n");
+
+    sb.append(ind(1) + "public void ASTNode.serialize(com.fasterxml.jackson.core.JsonGenerator g) throws SerializationException {\n");
+    sb.append(ind(2) + "serialize(g, null);\n");
+    sb.append(ind(1) + "}\n");
+
+    sb.append(ind(1) + "public void ASTNode.serialize(com.fasterxml.jackson.core.JsonGenerator g, String field) throws SerializationException {\n");
+    sb.append(ind(2) + "throw new SerializationException(\"unable to serialize class \" + this.getClass().getSimpleName());\n");
+    sb.append(ind(1) + "}\n");
+
+    sb.append(ind(1) + "public void ASTNode.serialize(java.io.File file) throws SerializationException {\n");
+    sb.append(ind(2) + "try {\n");
+    sb.append(ind(3) + "com.fasterxml.jackson.core.JsonFactory factory = new com.fasterxml.jackson.core.JsonFactory();\n");
+    sb.append(ind(3) + "com.fasterxml.jackson.core.JsonGenerator generator = factory.createGenerator(file, com.fasterxml.jackson.core.JsonEncoding.UTF8);\n");
+    sb.append(ind(3) + "generator.setPrettyPrinter(new com.fasterxml.jackson.core.util.DefaultPrettyPrinter());\n");
+    sb.append(ind(3) + "serialize(generator);\n");
+    sb.append(ind(3) + "generator.close();\n");
+    sb.append(ind(2) + "} catch (java.io.IOException e) {\n");
+    sb.append(ind(3) + "throw new SerializationException(\"Unable to serialize file \" + file.getAbsolutePath(), e);\n");
+    sb.append(ind(2) + "}\n");
+    sb.append(ind(1) + "}\n");
+
+    for (TypeDecl decl : getTypeDeclList()) {
+      if (!decl.isUnresolved()) {
+        decl.deserialize(sb);
+      }
+    }
+
+    sb.append("}\n");
+  }
+
+  public void Program.generateToJson(StringBuilder sb) {
+    sb.append("aspect ModelToJson {\n");
+
+    sb.append(ind(1) + "public class SerializationException extends Exception {\n");
+    sb.append(ind(2) + "public SerializationException(String message) {\n");
+    sb.append(ind(3) + "super(message);\n");
+    sb.append(ind(2) + "}\n");
+    sb.append(ind(2) + "public SerializationException(String message, Exception cause) {\n");
+    sb.append(ind(3) + "super(message, cause);\n");
+    sb.append(ind(2) + "}\n");
+    sb.append(ind(1) + "}\n");
+
+    for (TypeDecl decl : getTypeDeclList()) {
+      if (!decl.isUnresolved()) {
+        decl.serialize(sb);
+      }
+    }
+
+    sb.append("}\n");
+  }
+
+   public void TypeDecl.serialize(StringBuilder sb) {
+    sb.append(ind(1) + "public void " + getID() + ".serialize(com.fasterxml.jackson.core.JsonGenerator g, String fieldName) throws SerializationException {\n");
+    sb.append(ind(2) + "try {\n");
+    sb.append(ind(3) + "if (fieldName == null) {\n");
+    sb.append(ind(4) + "g.writeStartObject();\n");
+    sb.append(ind(3) + "} else {\n");
+    sb.append(ind(4) + "g.writeObjectFieldStart(fieldName);\n");
+    sb.append(ind(3) + "}\n");
+    sb.append(ind(3) + "g.writeStringField(\"" + jsonTypeKey + "\", \"" + getID() + "\");\n");
+    sb.append(ind(3) + "g.writeStringField(\"id\", __uid());\n");
+    if (componentsTransitive().size() > 0) {
+      sb.append(ind(3) + "g.writeObjectFieldStart(\"children\");\n");
+      for (Component child : componentsTransitive()) {
+        child.serialize(sb, 3);
+      }
+      sb.append(ind(3) + "g.writeEndObject(); // children\n");
+    }
+    if (relationComponents().size() > 0) {
+      sb.append(ind(3) + "g.writeObjectFieldStart(\"relations\");\n");
+      for (RelationComponent relation : relationComponents()) {
+        relation.serialize(sb, 3);
+      }
+      sb.append(ind(3) + "g.writeEndObject(); // relations\n");
+    }
+    sb.append(ind(3) + "g.writeEndObject();\n");
+    sb.append(ind(2) + "} catch (java.io.IOException e) {\n");
+    sb.append(ind(3) + "throw new SerializationException(\"unable to serialize " + getID() + "\", e);\n");
+    sb.append(ind(2) + "}\n");
+    sb.append(ind(1) + "}\n");
+
+  }
+
+  public abstract void Component.serialize(StringBuilder sb, int indent);
+
+  public void NormalComponent.serialize(StringBuilder sb, int indent) {
+    sb.append(ind(indent) + "get" + getID() + "().serialize(g, \"" + getID() + "\");\n");
+  }
+
+  public void OptComponent.serialize(StringBuilder sb, int indent) {
+    sb.append(ind(indent) + "if (has" + getID() + "()) {\n");
+    sb.append(ind(indent + 1) + "get" + getID() + "().serialize(g, \"" + getID() + "\");\n");
+    sb.append(ind(indent) + "}\n");
+  }
+
+  public void TokenComponent.serialize(StringBuilder sb, int indent) {
+
+    String type = getTypeUse().getID();
+
+    switch (type) {
+      case "float":
+      case "Float":
+      case "double":
+      case "Double":
+      case "int":
+      case "Integer":
+      case "short":
+      case "Short":
+      case "long":
+      case "Long":
+      case "byte":
+      case "Byte":
+        sb.append(ind(indent) + "g.writeNumberField(\"" + getID() + "\", get" + getID() + "());\n");
+        break;
+      case "boolean":
+      case "Boolean":
+        sb.append(ind(indent) + "g.writeBooleanField(\"" + getID() + "\", get" + getID() + "());\n");
+        break;
+      case "char":
+      case "Character":
+        sb.append(ind(indent) + "g.writeStringField(\"" + getID() + "\", Character.toString(get"+ getID() + "()));\n");
+        break;
+      case "String":
+        sb.append(ind(indent) + "g.writeStringField(\"" + getID() + "\", get" + getID() + "());\n");
+        break;
+      case "java.time.Instant":
+        sb.append(ind(indent) + "g.writeStringField(\"" + getID() + "\", get" + getID() + "()).toString());\n");
+        break;
+      case "java.time.Period":
+        sb.append(ind(indent) + "g.writeStringField(\"" + getID() + "\", get" + getID() + "()).toString());\n");
+        break;
+      default:
+        // assume that the type is an enum. there is no way of checking this here
+        sb.append(ind(indent) + "g.writeStringField(\"" + getID() + "\", get" + getID() + "()).name());\n");
+        // sb.append("throw new DeserializationException(\"Unable to deserialize child node of type \"" + getTypeUse() + "\"\")")
+    }
+  }
+
+  public void NTAComponent.serialize(StringBuilder sb, int indent) {
+    // do not serialize NTA
+  }
+
+  public void ListComponent.serialize(StringBuilder sb, int indent) {
+    sb.append(ind(indent) + "if (getNum" + getID() + "() > 0) {\n");
+    sb.append(ind(indent + 1) + "g.writeArrayFieldStart(\"" + getID() + "\");\n");
+    sb.append(ind(indent + 1) + "for (" + getTypeUse().decl().getID() + " child : get" + getID() + "List()) {\n");
+    sb.append(ind(indent + 2) + "child.serialize(g);\n");
+    sb.append(ind(indent + 1) + "}\n");
+    sb.append(ind(indent + 1) + "g.writeEndArray();\n");
+    sb.append(ind(indent) + "}\n");
+  }
+
+  public void OneRelationComponent.serialize(StringBuilder sb, int indent) {
+    sb.append(ind(indent) + "g.writeStringField(\"" + getID() + "\", get" + getID() + "().__uid());\n");
+  }
+
+  public void OptionalRelationComponent.serialize(StringBuilder sb, int indent) {
+    sb.append(ind(indent) + "if (has" + getID() + "()) {\n");
+    sb.append(ind(indent + 1) + "g.writeStringField(\"" + getID() + "\", get" + getID() + "().__uid());\n");
+    sb.append(ind(indent) + "}\n");
+  }
+
+  public void ManyRelationComponent.serialize(StringBuilder sb, int indent) {
+    sb.append(ind(indent) + "g.writeArrayFieldStart(\"" + getID() + "\");\n");
+    sb.append(ind(indent) + "for (" + ofTypeDecl().getID() + " child : get" + getID() + "List()) {\n");
+    sb.append(ind(indent + 1) + "g.writeString(child.__uid());\n");
+    sb.append(ind(indent) + "}\n");
+    sb.append(ind(indent) + "g.writeEndArray();\n");
+  }
+
+  public void TypeDecl.deserialize(StringBuilder sb) {
+
+    sb.append(ind(1) + "public static " + getID() + " " + getID() + ".deserialize(java.io.File file) throws DeserializationException {\n");
+    sb.append(ind(2) + "try {\n");
+    sb.append(ind(3) + "com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();\n");
+    sb.append(ind(3) + "com.fasterxml.jackson.core.JsonFactory factory = mapper.getFactory();\n");
+    sb.append(ind(3) + "com.fasterxml.jackson.core.JsonParser parser = factory.createParser(file);\n");
+    sb.append(ind(3) + "return deserialize((com.fasterxml.jackson.databind.JsonNode)mapper.readTree(parser));\n");
+    sb.append(ind(2) + "} catch (java.io.IOException e) {\n");
+    sb.append(ind(3) + "throw new DeserializationException(\"unable to deserialize \" + file.getAbsolutePath(), e);\n");
+    sb.append(ind(2) + "}\n");
+    sb.append(ind(1) + "}\n");
+
+    sb.append(ind(1) + "public static " + getID() + " " + getID() + ".deserialize(" + jsonNodeType + " node) throws DeserializationException {\n");
+    sb.append(ind(2) + getID() + " element;\n");
+    if (getAbstract()) {
+      // switch case between all implementations of the abstract class
+      sb.append(ind(2) + "switch (node" + jsonNodeTypeAccessor + ") {\n");
+      for (TypeDecl subType : subTypeDecls()) {
+        sb.append(ind(3) + "case \"" + subType.getID() + "\":\n");
+        sb.append(ind(4) + "element = " + subType.getID() + ".deserialize(node);\n");
+        sb.append(ind(4) + "break;\n");
+      }
+      sb.append(ind(3) + "default:\n");
+      sb.append(ind(4) + "throw new DeserializationException(\"Unable to deserialize child of unexpected type \" + node" + jsonNodeTypeAccessor + " + \"(" + getID() + " expected)\");\n");
+      sb.append(ind(2) + "}\n");
+    } else {
+      sb.append(ind(2) + "element = new " + getID() + "();\n");
+    }
+
+    // deserialize id
+    sb.append(ind(2) + "if (node.has(\"id\")) {\n");
+    sb.append(ind(3) + "element.__uid = node.get(\"id\").asText();\n");
+    sb.append(ind(2) + "}\n");
+
+    // deserialize containment children
+    if (componentsTransitive().size() > 0) {
+      sb.append(ind(2) + "if (node.has(\"children\")) {\n");
+      sb.append(ind(3) + jsonNodeType + " children = node.get(\"children\");\n");
+      for (Component component : componentsTransitive()) {
+        sb.append(ind(3) + "if (children.has(\"" + component.getID() + "\")) {\n");
+        component.deserialize(sb, 4);
+        sb.append(ind(3) + "}\n");
+      }
+      sb.append(ind(2) + "}\n");
+    }
+    // deserialize non-containment children
+    Set<RelationComponent> relationComponents = relationComponents();
+    if (relationComponents.size() > 0) {
+      sb.append(ind(2) + "if (node.has(\"relations\")) {\n");
+      sb.append(ind(3) + jsonNodeType + " relations = node.get(\"relations\");\n");
+      for (RelationComponent component : relationComponents) {
+        sb.append(ind(3) + "if (relations.has(\"" + component.getID() + "\")) {\n");
+        component.deserialize(sb, 4);
+        sb.append(ind(3) + "}\n");
+      }
+      sb.append(ind(2) + "}\n");
+    }
+
+    sb.append(ind(2) + "return element;\n");
+    sb.append(ind(1) + "}\n");
+
+  }
+
+  public abstract void Component.deserialize(StringBuilder sb, int indent);
+
+  public void NormalComponent.deserialize(StringBuilder sb, int indent) {
+    sb.append(ind(indent) + "element.set" + getID() + "(" + getTypeUse().decl().getID() + ".deserialize(children.get(\"" + getID() + "\")));\n");
+    sb.append(ind(indent - 1) + "} else {\n");
+    sb.append(ind(indent) + "throw new DeserializationException(\"deserializer of missing mandatory child " + getID() + "\");\n");
+  }
+
+  public void OptComponent.deserialize(StringBuilder sb, int indent) {
+    sb.append(ind(indent + 1) + "element.set" + getID() + "(" + getTypeUse().decl().getID() + ".deserialize(children.get(\"" + getID() + "\")));\n");
+  }
+
+  public void TokenComponent.deserialize(StringBuilder sb, int indent) {
+
+    String type = getTypeUse().getID();
+
+    switch (type) {
+      case "float":
+      case "Float":
+        sb.append(ind(indent) + "element.set" + getID() + "(Float.valueOf(children.get(\"" + getID() + "\").asText()));\n");
+        break;
+      case "double":
+      case "Double":
+        sb.append(ind(indent) + "element.set" + getID() + "(children.get(\"" + getID() + "\").asDouble());\n");
+        break;
+      case "int":
+      case "Integer":
+        sb.append(ind(indent) + "element.set" + getID() + "(children.get(\"" + getID() + "\").asInt());\n");
+        break;
+      case "short":
+      case "Short":
+        sb.append(ind(indent) + "element.set" + getID() + "(Short.valueOf(children.get(\"" + getID() + "\").asText()));\n");
+        break;
+      case "long":
+      case "Long":
+        sb.append(ind(indent) + "element.set" + getID() + "(children.get(\"" + getID() + "\").asLong());\n");
+        break;
+      case "byte":
+      case "Byte":
+        sb.append(ind(indent) + "element.set" + getID() + "(Byte.valueOf(children.get(\"" + getID() + "\").asText()));\n");
+        break;
+      case "boolean":
+      case "Boolean":
+        sb.append(ind(indent) + "element.set" + getID() + "(children.get(\"" + getID() + "\").asBoolean());\n");
+        break;
+      case "char":
+      case "Character":
+        sb.append(ind(indent) + "String chars = children.get(\"" + getID() + "\").asText();\n");
+        sb.append(ind(indent) + "if (chars.length() == 1) {\n");
+        sb.append(ind(indent + 2) + "element.set" + getID() + "(chars.charAt(0))\n");
+        sb.append(ind(indent) + "} else {\n");
+        sb.append(ind(indent + 2) + "throw new DeserializationException(\"unable to deserialize char '\" + chars + \"'\");\n");
+        sb.append(ind(indent) + "}\n");
+        break;
+      case "String":
+        sb.append(ind(indent) + "element.set" + getID() + "(children.get(\"" + getID() + "\").asText());\n");
+        break;
+      case "java.time.Instant":
+        sb.append(ind(indent) + "element.set" + getID() + "(java.time.Instant.parse(children.get(\"" + getID() + "\").asText()));\n");
+        break;
+      case "java.time.Period":
+        sb.append(ind(indent) + "element.set" + getID() + "(java.time.Period.parse(children.get(\"" + getID() + "\").asText()));\n");
+        break;
+      default:
+        // assume that the type is an enum. there is no way of checking this here
+        sb.append(ind(indent) + "element.set" + getID() + "Enum.valueOf(" + type + ".getClass(), children.get(\"" + getID() + "\".asText())));\n");
+
+        // sb.append("throw new DeserializationException(\"Unable to deserialize child node of type \"" + getTypeUse() + "\"\")")
+    }
+  }
+
+  public void Program.writeUID(StringBuilder sb) {
+    sb.append("aspect UID {\n");
+    sb.append(ind(1) + "class UIDProvider {\n");
+    sb.append(ind(2) + "private static long nextUID = 0;\n");
+    sb.append(ind(2) + "public static String getUID() {\n");
+    sb.append(ind(3) + "return String.valueOf(nextUID++);\n");
+    sb.append(ind(2) + "}\n");
+    sb.append(ind(1) + "}\n");
+    sb.append("\n");
+    sb.append(ind(1) + "protected String ASTNode.__uid = null;\n");
+    sb.append("\n");
+    sb.append(ind(1) + "protected String ASTNode.__uid() {\n");
+    sb.append(ind(2) + "String customUID = serializationID();\n");
+    sb.append(ind(2) + "if (customUID == null) {\n");
+    sb.append(ind(3) + "if (__uid == null) {\n");
+    sb.append(ind(4) + "__uid = UIDProvider.getUID();\n");
+    sb.append(ind(3) + "}\n");
+    sb.append(ind(3) + "return __uid;\n");
+    sb.append(ind(2) + "} else {\n");
+    sb.append(ind(3) + "return customUID;\n");
+    sb.append(ind(2) + "}\n");
+    sb.append(ind(1) + "}\n");
+    sb.append("\n");
+    sb.append(ind(1) + "protected String ASTNode.serializationID() {\n");
+    sb.append(ind(2) + "return null;\n");
+    sb.append(ind(1) + "}\n");
+    sb.append("}\n");
+  }
+
+  public void NTAComponent.deserialize(StringBuilder sb, int indent) {
+    // do not serialize NTA
+  }
+
+  public void ListComponent.deserialize(StringBuilder sb, int indent) {
+    sb.append(ind(indent) + "for (" + jsonNodeType + " child : children.get(\"" + getID() + "\")) {\n");
+    sb.append(ind(indent + 1) + "element.add" + getID() + "(" + getTypeUse().decl().getID() + ".deserialize(child));\n");
+    sb.append(ind(indent) + "}\n");
+  }
+
+  public void OneRelationComponent.deserialize(StringBuilder sb, int indent) {
+    sb.append(ind(indent) + "element.set" + getID() + "(" + ofTypeDecl().getID() + ".createRefDirection(relations.get(\"" + getID() + "\").asText()));\n");
+    sb.append(ind(indent - 1) + "} else {\n");
+    sb.append(ind(indent) + "throw new DeserializationException(\"deserializer of missing mandatory relation child " + getID() + "\");\n");
+  }
+
+  public void OptionalRelationComponent.deserialize(StringBuilder sb, int indent) {
+    sb.append(ind(indent) + "element.set" + getID() + "(" + ofTypeDecl().getID() + ".createRefDirection(relations.get(\"" + getID() + "\").asText()));\n");
+  }
+
+  public void ManyRelationComponent.deserialize(StringBuilder sb, int indent) {
+    sb.append(ind(indent) + "for (" + jsonNodeType + " child : relations.get(\"" + getID() + "\")) {\n");
+    sb.append(ind(indent + 1) + "element.add" + getID() + "(" + ofTypeDecl().getID() + ".createRefDirection(child.asText()));\n");
+    sb.append(ind(indent) + "}\n");
+  }
+}
+
 aspect PrettyPrint {
   public String Relation.prettyPrint() {
     return "rel "
diff --git a/src/main/java/org/jastadd/relast/compiler/Compiler.java b/src/main/java/org/jastadd/relast/compiler/Compiler.java
index 135a34b6ce4e4d4bf0dfd734351f54922ea9ae9c..9c6c23bc753d3516e2a639d9e9beb2e51c2e2eb5 100644
--- a/src/main/java/org/jastadd/relast/compiler/Compiler.java
+++ b/src/main/java/org/jastadd/relast/compiler/Compiler.java
@@ -2,16 +2,14 @@ package org.jastadd.relast.compiler;
 
 import beaver.Parser;
 import org.jastadd.relast.ast.*;
-import org.jastadd.relast.compiler.options.CommandLine;
+import org.jastadd.relast.compiler.options.*;
 import org.jastadd.relast.compiler.options.CommandLine.CommandLineException;
-import org.jastadd.relast.compiler.options.FlagOption;
-import org.jastadd.relast.compiler.options.Option;
-import org.jastadd.relast.compiler.options.StringOption;
 import org.jastadd.relast.parser.RelAstParser;
 import org.jastadd.relast.scanner.RelAstScanner;
 
 import java.io.*;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 public class Compiler {
@@ -21,6 +19,7 @@ public class Compiler {
   private StringOption optionListClass;
   private StringOption optionJastAddList;
   private StringOption optionGrammarName;
+  private EnumOption optionSerializer;
   private FlagOption optionResolverHelper;
   private CommandLine commandLine;
 
@@ -45,7 +44,7 @@ public class Compiler {
     }
 
 
-    if (optionResolverHelper.isSet()) {
+    if (optionResolverHelper.isSet() || optionSerializer.isSet()) {
       // get a list of all (abstract or not) non-terminals
       List<TypeDecl> nonTerminals = new ArrayList<>();
       for (TypeDecl typeDecl : p.getTypeDecls()) {
@@ -88,8 +87,21 @@ public class Compiler {
 
       if (optionWriteToFile.isSet()) {
 
+        if (optionSerializer.isSet()) {
+          ASTNode.serializer = true;
+          switch (optionSerializer.getValue()) {
+            case "jackson":
+              writeToFile(grammarName + "Serializer.jadd", p.generateSerializer());
+              break;
+          }
+        }
+
         if (optionResolverHelper.isSet()) {
           ASTNode.resolverHelper = true;
+          writeToFile(grammarName + "ResolverStubs.jrag", p.generateResolverStubs());
+        }
+
+        if (optionSerializer.isSet() || optionResolverHelper.isSet()) {
           writeToFile(grammarName + "RefResolver.jadd", p.generateRewriteToSuperTypeStub());
         }
 
@@ -131,6 +143,7 @@ public class Compiler {
     optionGrammarName = addOption(new StringOption("grammarName", "name of the generated grammar and aspect (without file extension)"));
     optionResolverHelper = addOption(new FlagOption("resolverHelper", "create a subtype for each type containing a string that can be used to resolve the type later"));
     optionJastAddList = addOption(new StringOption("jastAddList", "set the name of the List type in JastAdd (has to match the option '--List' or its default List)"));
+    optionSerializer = addOption(new EnumOption("serializer", "generate a (de-)serializer", Arrays.asList("jackson"), "jackson"));
 
   }
 
diff --git a/src/test/jastadd/relations/Relations2.ast b/src/test/jastadd/relations/Relations2.ast
deleted file mode 100644
index 4bd2e3a4b7a7d96773f46dad412c46b0157abbf5..0000000000000000000000000000000000000000
--- a/src/test/jastadd/relations/Relations2.ast
+++ /dev/null
@@ -1,3 +0,0 @@
-Root ::= A* B*;
-A ::= <Name:String> <_impl_Di2:B> <_impl_Bi2:B> <_impl_Bi4:B> <_impl_Bi3:B> <_impl_Bi8:ArrayList<B>> <_impl_Bi9:ArrayList<B>> <_impl_Bi1:B> <_impl_Bi5:B> <_impl_Di3:ArrayList<B>> <_impl_Bi7:ArrayList<B>> <_impl_Bi6:B> <_impl_Di1:B>;
-B ::= <Name:String> <_impl_Bi7:A> <_impl_Bi3:ArrayList<A>> <_impl_Bi5:A> <_impl_Bi1:A> <_impl_Bi6:ArrayList<A>> <_impl_Bi9:ArrayList<A>> <_impl_Bi2:A> <_impl_Bi4:A> <_impl_Bi8:A>;
diff --git a/src/test/jastadd/relations/Relations2.jadd b/src/test/jastadd/relations/Relations2.jadd
deleted file mode 100644
index 3fed2f9af474b969c9a97b1926f186c452316767..0000000000000000000000000000000000000000
--- a/src/test/jastadd/relations/Relations2.jadd
+++ /dev/null
@@ -1,43 +0,0 @@
-import java.util.ArrayList;
-import java.util.Collections;
-aspect RelAstAPI {
-  public boolean ASTNode.violatesLowerBounds() {
-    return !getLowerBoundsViolations().isEmpty();
-  }
-  public java.util.List<Pair<ASTNode, String>> ASTNode.getLowerBoundsViolations() {
-    ArrayList<Pair<ASTNode, String>> list = new ArrayList<>();
-    computeLowerBoundsViolations(list);
-    return list;
-  }
-  public void ASTNode.computeLowerBoundsViolations(java.util.List<Pair<ASTNode, String>> list) {
-    for (int i = 0; i < getNumChildNoTransform(); i++) {
-      getChildNoTransform(i).computeLowerBoundsViolations(list);
-    }
-  }
-  public class Pair<T1, T2> {
-    public final T1 _1;
-    public final T2 _2;
-    public Pair(T1 _1, T2 _2) {
-      ASTNode.assertNotNull(_1);
-      ASTNode.assertNotNull(_2);
-      this._1 = _1;
-      this._2 = _2;
-    }
-    public boolean equals(Object other) {
-      if (other instanceof Pair) {
-        Pair<?,?> p = (Pair<?,?>) other;
-        return _1.equals(p._1) && _2.equals(p._2);
-      } else {
-        return false;
-      }
-    }
-    public int hashCode() {
-      return 31*_1.hashCode() + _2.hashCode();
-    }
-  }
-  public static void ASTNode.assertNotNull(Object obj) {
-    if (obj == null) {
-      throw new NullPointerException();
-    }
-  }
-}
diff --git a/src/test/jastadd/serializer/Serializer.relast b/src/test/jastadd/serializer/Serializer.relast
new file mode 100644
index 0000000000000000000000000000000000000000..41c9e621faf9cea7c6de73768b049b1927aab1b2
--- /dev/null
+++ b/src/test/jastadd/serializer/Serializer.relast
@@ -0,0 +1,21 @@
+
+Root ::= A* B* C;
+A:NamedElement;
+B:NamedElement;
+C:NamedElement ::= D1:D [D2:D] D3:D* <F1:float> <F2:Float>;
+D:NamedElement;
+
+abstract NamedElement ::= <Name>;
+
+rel A.Di1  -> B;
+rel A.Di2? -> B;
+rel A.Di3* -> B;
+
+rel A.Bi1 <-> B.Bi1;
+rel A.Bi2 <-> B.Bi2?;
+rel A.Bi3 <-> B.Bi3*;
+
+rel A.Bi5? <-> B.Bi5?;
+rel A.Bi6? <-> B.Bi6*;
+
+rel A.Bi9* <-> B.Bi9*;
diff --git a/src/test/java/org/jastadd/relast/tests/Serializer.java b/src/test/java/org/jastadd/relast/tests/Serializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..5bfa84f5f51481f87dc5a81e060a7d51fdbc9545
--- /dev/null
+++ b/src/test/java/org/jastadd/relast/tests/Serializer.java
@@ -0,0 +1,98 @@
+package org.jastadd.relast.tests;
+
+import org.junit.jupiter.api.Test;
+import serializer.ast.*;
+
+import java.io.File;
+
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+class Serializer {
+
+
+  @Test
+  void testDi1() throws SerializationException, DeserializationException {
+
+    Root r = new Root();
+    A a1 = new A("a1");
+    A a2 = new A("a2");
+    A a3 = new A("a3");
+    B b1 = new B("b1");
+    B b2 = new B("b2");
+    B b3 = new B("b3");
+    C c = new C();
+    c.setName("c");
+
+    // non-terminals
+    c.setD1(new D("d1"));
+    c.setD2(new D("d2"));
+    c.addD3(new D("D3-1"));
+    c.addD3(new D("D3-2"));
+    c.addD3(new D("D3-3"));
+
+    // tokens
+    c.setF1(42f);
+    c.setF2(42.13f);
+
+    r.setC(c);
+
+    // non-containment relations
+    r.addA(a1);
+    r.addA(a2);
+    r.addA(a3);
+    r.addB(b1);
+    r.addB(b2);
+    r.addB(b3);
+
+    // Di1
+    a1.setDi1(b2);
+    a2.setDi1(b1);
+    a3.setDi1(b3);
+
+    // Di2
+    a1.setDi2(b2);
+    a3.setDi2(b1);
+
+    // Di3
+    a1.addDi3(b1);
+    a1.addDi3(b2);
+    a1.addDi3(b3);
+    a2.addDi3(b2);
+
+    // Bi1
+    a1.setBi1(b1);
+    a2.setBi1(b3);
+    a3.setBi1(b2);
+
+    // Bi2
+    a1.setBi2(b1);
+    a2.setBi2(b2);
+    a3.setBi2(b3);
+
+    // Bi3
+    a1.setBi3(b2);
+    a2.setBi3(b2);
+    a3.setBi3(b2);
+
+    // Bi5
+    a1.setBi5(b1);
+    a2.setBi5(b3);
+
+    // Bi6
+    a2.setBi6(b2);
+    a3.setBi6(b2);
+
+    // Bi9
+    a1.addBi9(b1);
+    a1.addBi9(b2);
+    a2.addBi9(b2);
+
+    File f = new File("test.json");
+    r.serialize(f);
+
+    Root copy = Root.deserialize(f);
+    File f2 = new File("copy.json");
+    copy.serialize(f2);
+  }
+
+}