Moved all ContentModel parsing to Parser classes
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/DocumentData.java b/lottie/src/main/java/com/airbnb/lottie/model/DocumentData.java
index 4e57503..39c987f 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/DocumentData.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/DocumentData.java
@@ -1,11 +1,6 @@
 package com.airbnb.lottie.model;
 
 import android.support.annotation.ColorInt;
-import android.util.JsonReader;
-
-import com.airbnb.lottie.utils.JsonUtils;
-
-import java.io.IOException;
 
 public class DocumentData {
 
@@ -22,7 +17,7 @@
   public boolean strokeOverFill;
 
 
-  DocumentData(String text, String fontName, double size, int justification, int tracking,
+  public DocumentData(String text, String fontName, double size, int justification, int tracking,
       double lineHeight, double baselineShift, @ColorInt int color, @ColorInt int strokeColor,
       int strokeWidth, boolean strokeOverFill) {
     this.text = text;
@@ -38,72 +33,6 @@
     this.strokeOverFill = strokeOverFill;
   }
 
-
-  public static final class Factory {
-
-    private Factory() {
-    }
-
-    public static DocumentData newInstance(JsonReader reader) throws IOException {
-      String text = null;
-      String fontName = null;
-      double size = 0;
-      int justification = 0;
-      int tracking = 0;
-      double lineHeight = 0;
-      double baselineShift = 0;
-      int fillColor = 0;
-      int strokeColor = 0;
-      int strokeWidth = 0;
-      boolean strokeOverFill = true;
-
-      reader.beginObject();
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "t":
-            text = reader.nextString();
-            break;
-          case "f":
-            fontName = reader.nextString();
-            break;
-          case "s":
-            size = reader.nextDouble();
-            break;
-          case "j":
-            justification = reader.nextInt();
-            break;
-          case "tr":
-            tracking = reader.nextInt();
-            break;
-          case "lh":
-            lineHeight = reader.nextDouble();
-            break;
-          case "ls":
-            baselineShift = reader.nextDouble();
-            break;
-          case "fc":
-            fillColor = JsonUtils.jsonToColor(reader);
-            break;
-          case "sc":
-            strokeColor = JsonUtils.jsonToColor(reader);
-            break;
-          case "sw":
-            strokeWidth = reader.nextInt();
-            break;
-          case "of":
-            strokeOverFill = reader.nextBoolean();
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-      reader.endObject();
-
-      return new DocumentData(text, fontName, size, justification, tracking, lineHeight,
-          baselineShift, fillColor, strokeColor, strokeWidth, strokeOverFill);
-    }
-  }
-
   @Override public int hashCode() {
     int result;
     long temp;
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/Font.java b/lottie/src/main/java/com/airbnb/lottie/model/Font.java
index f1207c2..d4beb6c 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/Font.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/Font.java
@@ -1,9 +1,5 @@
 package com.airbnb.lottie.model;
 
-import android.util.JsonReader;
-
-import java.io.IOException;
-
 public class Font {
 
   private final String family;
@@ -11,7 +7,7 @@
   private final String style;
   private final float ascent;
 
-  Font(String family, String name, String style, float ascent) {
+  public Font(String family, String name, String style, float ascent) {
     this.family = family;
     this.name = name;
     this.style = style;
@@ -33,37 +29,4 @@
   @SuppressWarnings("unused") float getAscent() {
     return ascent;
   }
-
-  public static class Factory {
-
-    public static Font newInstance(JsonReader reader) throws IOException {
-      String family = null;
-      String name = null;
-      String style = null;
-      float ascent = 0;
-
-      reader.beginObject();
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "fFamily":
-            family = reader.nextString();
-            break;
-          case "fName":
-            name = reader.nextString();
-            break;
-          case "fStyle":
-            style = reader.nextString();
-            break;
-          case "ascent":
-            ascent = (float) reader.nextDouble();
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-      reader.endObject();
-
-      return new Font(family, name, style, ascent);
-    }
-  }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/FontCharacter.java b/lottie/src/main/java/com/airbnb/lottie/model/FontCharacter.java
index aa66e81..9415b50 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/FontCharacter.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/FontCharacter.java
@@ -1,12 +1,7 @@
 package com.airbnb.lottie.model;
 
-import android.util.JsonReader;
-
-import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.content.ShapeGroup;
 
-import java.io.IOException;
-import java.util.ArrayList;
 import java.util.List;
 
 public class FontCharacter {
@@ -26,7 +21,7 @@
   private final String style;
   private final String fontFamily;
 
-  FontCharacter(List<ShapeGroup> shapes, char character, int size,
+  public FontCharacter(List<ShapeGroup> shapes, char character, int size,
       double width, String style, String fontFamily) {
     this.shapes = shapes;
     this.character = character;
@@ -52,60 +47,6 @@
     return style;
   }
 
-  public static class Factory {
-
-    public static FontCharacter newInstance(
-        JsonReader reader, LottieComposition composition) throws IOException {
-      char character = '\0';
-      int size = 0;
-      double width = 0;
-      String style = null;
-      String fontFamily = null;
-      List<ShapeGroup> shapes = new ArrayList<>();
-
-      reader.beginObject();
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "ch":
-            character = reader.nextString().charAt(0);
-            break;
-          case "size":
-            size = reader.nextInt();
-            break;
-          case "w":
-            width = reader.nextDouble();
-            break;
-          case "style":
-            style = reader.nextString();
-            break;
-          case "fFamily":
-            fontFamily = reader.nextString();
-            break;
-          case "data":
-            reader.beginObject();
-            while (reader.hasNext()) {
-              if ("shapes".equals(reader.nextName())) {
-                reader.beginArray();
-                while (reader.hasNext()) {
-                  shapes.add((ShapeGroup) ShapeGroup.shapeItemWithJson(reader, composition));
-                }
-                reader.endArray();
-              } else {
-                reader.skipValue();
-              }
-            }
-            reader.endObject();
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-      reader.endObject();
-
-      return new FontCharacter(shapes, character, size, width, style, fontFamily);
-    }
-  }
-
   @Override public int hashCode() {
     return hashFor(character, fontFamily, style);
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTextFrame.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTextFrame.java
index a1b5ffe..8edb499 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTextFrame.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTextFrame.java
@@ -6,6 +6,7 @@
 import com.airbnb.lottie.animation.Keyframe;
 import com.airbnb.lottie.animation.keyframe.TextKeyframeAnimation;
 import com.airbnb.lottie.model.DocumentData;
+import com.airbnb.lottie.parser.DocumentDataParser;
 
 import java.io.IOException;
 import java.util.List;
@@ -41,7 +42,7 @@
 
     @Override
     public DocumentData valueFromObject(JsonReader reader, float scale) throws IOException {
-      return DocumentData.Factory.newInstance(reader);
+      return DocumentDataParser.parse(reader);
     }
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/CircleShape.java b/lottie/src/main/java/com/airbnb/lottie/model/content/CircleShape.java
index f2ff8f0..49b5794 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/CircleShape.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/CircleShape.java
@@ -1,26 +1,21 @@
 package com.airbnb.lottie.model.content;
 
 import android.graphics.PointF;
-import android.util.JsonReader;
 
-import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
 import com.airbnb.lottie.animation.content.Content;
 import com.airbnb.lottie.animation.content.EllipseContent;
-import com.airbnb.lottie.model.animatable.AnimatablePathValue;
 import com.airbnb.lottie.model.animatable.AnimatablePointValue;
 import com.airbnb.lottie.model.animatable.AnimatableValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import java.io.IOException;
-
 public class CircleShape implements ContentModel {
   private final String name;
   private final AnimatableValue<PointF, PointF> position;
   private final AnimatablePointValue size;
   private final boolean isReversed;
 
-  private CircleShape(String name, AnimatableValue<PointF, PointF> position,
+  public CircleShape(String name, AnimatableValue<PointF, PointF> position,
       AnimatablePointValue size, boolean isReversed) {
     this.name = name;
     this.position = position;
@@ -32,42 +27,6 @@
     return new EllipseContent(drawable, layer, this);
   }
 
-  static class Factory {
-    private Factory() {
-    }
-
-    static CircleShape newInstance(
-        JsonReader reader, LottieComposition composition) throws IOException {
-      String name = null;
-      AnimatableValue<PointF, PointF> position = null;
-      AnimatablePointValue size = null;
-      boolean reversed = false;
-
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "nm":
-            name = reader.nextString();
-            break;
-          case "p":
-            position = AnimatablePathValue
-                .createAnimatablePathOrSplitDimensionPath(reader, composition);
-            break;
-          case "s":
-            size = AnimatablePointValue.Factory.newInstance(reader, composition);
-            break;
-          case "d":
-            // "d" is 2 for normal and 3 for reversed.
-            reversed = reader.nextInt() == 3;
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-
-      return new CircleShape(name, position, size, reversed);
-    }
-  }
-
   public String getName() {
     return name;
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/GradientFill.java b/lottie/src/main/java/com/airbnb/lottie/model/content/GradientFill.java
index 41ab93d..cf1ecdf 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/GradientFill.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/GradientFill.java
@@ -2,9 +2,7 @@
 
 import android.graphics.Path;
 import android.support.annotation.Nullable;
-import android.util.JsonReader;
 
-import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
 import com.airbnb.lottie.animation.content.Content;
 import com.airbnb.lottie.animation.content.GradientFillContent;
@@ -14,8 +12,6 @@
 import com.airbnb.lottie.model.animatable.AnimatablePointValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import java.io.IOException;
-
 public class GradientFill implements ContentModel {
 
   private final GradientType gradientType;
@@ -28,7 +24,7 @@
   @Nullable private final AnimatableFloatValue highlightLength;
   @Nullable private final AnimatableFloatValue highlightAngle;
 
-  private GradientFill(String name, GradientType gradientType, Path.FillType fillType,
+  public GradientFill(String name, GradientType gradientType, Path.FillType fillType,
       AnimatableGradientColorValue gradientColor,
       AnimatableIntegerValue opacity, AnimatablePointValue startPoint,
       AnimatablePointValue endPoint, AnimatableFloatValue highlightLength,
@@ -84,65 +80,4 @@
     return new GradientFillContent(drawable, layer, this);
   }
 
-  static class Factory {
-    private Factory() {
-    }
-
-    static GradientFill newInstance(
-        JsonReader reader, LottieComposition composition) throws IOException {
-      String name = null;
-      AnimatableGradientColorValue color = null;
-      AnimatableIntegerValue opacity = null;
-      GradientType gradientType = null;
-      AnimatablePointValue startPoint = null;
-      AnimatablePointValue endPoint = null;
-      Path.FillType fillType = null;
-
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "nm":
-            name = reader.nextString();
-            break;
-          case "g":
-            int points = -1;
-            reader.beginObject();
-            while (reader.hasNext()) {
-              switch (reader.nextName()) {
-                case "p":
-                  points = reader.nextInt();
-                  break;
-                case "k":
-                  color = AnimatableGradientColorValue.Factory
-                      .newInstance(reader, composition, points);
-                  break;
-                default:
-                  reader.skipValue();
-              }
-            }
-            reader.endObject();
-            break;
-          case "o":
-            opacity = AnimatableIntegerValue.Factory.newInstance(reader, composition);
-            break;
-          case "t":
-            gradientType = reader.nextInt() == 1 ? GradientType.Linear : GradientType.Radial;
-            break;
-          case "s":
-            startPoint = AnimatablePointValue.Factory.newInstance(reader, composition);
-            break;
-          case "e":
-            endPoint = AnimatablePointValue.Factory.newInstance(reader, composition);
-            break;
-          case "r":
-            fillType = reader.nextInt() == 1 ? Path.FillType.WINDING : Path.FillType.EVEN_ODD;
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-
-      return new GradientFill(name, gradientType, fillType, color, opacity, startPoint, endPoint,
-          null, null);
-    }
-  }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/GradientStroke.java b/lottie/src/main/java/com/airbnb/lottie/model/content/GradientStroke.java
index 5bc70fa..bcb6f77 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/GradientStroke.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/GradientStroke.java
@@ -1,9 +1,7 @@
 package com.airbnb.lottie.model.content;
 
 import android.support.annotation.Nullable;
-import android.util.JsonReader;
 
-import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
 import com.airbnb.lottie.animation.content.Content;
 import com.airbnb.lottie.animation.content.GradientStrokeContent;
@@ -13,8 +11,6 @@
 import com.airbnb.lottie.model.animatable.AnimatablePointValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import java.io.IOException;
-import java.util.ArrayList;
 import java.util.List;
 
 public class GradientStroke implements ContentModel {
@@ -31,7 +27,7 @@
   private final List<AnimatableFloatValue> lineDashPattern;
   @Nullable private final AnimatableFloatValue dashOffset;
 
-  private GradientStroke(String name, GradientType gradientType,
+  public GradientStroke(String name, GradientType gradientType,
       AnimatableGradientColorValue gradientColor,
       AnimatableIntegerValue opacity, AnimatablePointValue startPoint,
       AnimatablePointValue endPoint, AnimatableFloatValue width, ShapeStroke.LineCapType capType,
@@ -102,106 +98,6 @@
     private Factory() {
     }
 
-    static GradientStroke newInstance(
-        JsonReader reader, LottieComposition composition) throws IOException {
-      String name = null;
-      AnimatableGradientColorValue color = null;
-      AnimatableIntegerValue opacity = null;
-      GradientType gradientType = null;
-      AnimatablePointValue startPoint = null;
-      AnimatablePointValue endPoint = null;
-      AnimatableFloatValue width = null;
-      ShapeStroke.LineCapType capType = null;
-      ShapeStroke.LineJoinType joinType = null;
-      AnimatableFloatValue offset = null;
 
-
-      List<AnimatableFloatValue> lineDashPattern = new ArrayList<>();
-
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "nm":
-            name = reader.nextString();
-            break;
-          case "g":
-            int points = -1;
-            reader.beginObject();
-            while (reader.hasNext()) {
-              switch (reader.nextName()) {
-                case "p":
-                  points = reader.nextInt();
-                  break;
-                case "k":
-                  color = AnimatableGradientColorValue.Factory
-                      .newInstance(reader, composition, points);
-                  break;
-                default:
-                  reader.skipValue();
-              }
-            }
-            reader.endObject();
-            break;
-          case "o":
-            opacity = AnimatableIntegerValue.Factory.newInstance(reader, composition);
-            break;
-          case "t":
-            gradientType = reader.nextInt() == 1 ? GradientType.Linear : GradientType.Radial;
-            break;
-          case "s":
-            startPoint = AnimatablePointValue.Factory.newInstance(reader, composition);
-            break;
-          case "e":
-            endPoint = AnimatablePointValue.Factory.newInstance(reader, composition);
-            break;
-          case "w":
-            width = AnimatableFloatValue.Factory.newInstance(reader, composition);
-            break;
-          case "lc":
-            capType = ShapeStroke.LineCapType.values()[reader.nextInt() - 1];
-            break;
-          case "lj":
-            joinType = ShapeStroke.LineJoinType.values()[reader.nextInt() - 1];
-            break;
-          case "d":
-            reader.beginArray();
-            while (reader.hasNext()) {
-              String n = null;
-              AnimatableFloatValue val = null;
-              reader.beginObject();
-              while (reader.hasNext()) {
-                switch (reader.nextName()) {
-                  case "n":
-                    n = reader.nextString();
-                    break;
-                  case "v":
-                    val =  AnimatableFloatValue.Factory.newInstance(reader, composition);
-                    break;
-                  default:
-                    reader.skipValue();
-                }
-              }
-              reader.endObject();
-
-              if (n.equals("o")) {
-                offset = val;
-              } else if (n.equals("d") || n.equals("g")) {
-                lineDashPattern.add(val);
-              }
-            }
-            reader.endArray();
-            if (lineDashPattern.size() == 1) {
-              // If there is only 1 value then it is assumed to be equal parts on and off.
-              lineDashPattern.add(lineDashPattern.get(0));
-            }
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-
-      return new GradientStroke(
-          name, gradientType, color, opacity, startPoint, endPoint, width, capType, joinType,
-          lineDashPattern, offset);
-    }
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/MergePaths.java b/lottie/src/main/java/com/airbnb/lottie/model/content/MergePaths.java
index fd0c64f..5223cae 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/MergePaths.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/MergePaths.java
@@ -1,7 +1,6 @@
 package com.airbnb.lottie.model.content;
 
 import android.support.annotation.Nullable;
-import android.util.JsonReader;
 import android.util.Log;
 
 import com.airbnb.lottie.L;
@@ -10,8 +9,6 @@
 import com.airbnb.lottie.animation.content.MergePathsContent;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import java.io.IOException;
-
 
 public class MergePaths implements ContentModel {
 
@@ -22,7 +19,7 @@
     Intersect,
     ExcludeIntersections;
 
-    private static MergePathsMode forId(int id) {
+    public static MergePathsMode forId(int id) {
       switch (id) {
         case 1:
           return Merge;
@@ -43,7 +40,7 @@
   private final String name;
   private final MergePathsMode mode;
 
-  private MergePaths(String name, MergePathsMode mode) {
+  public MergePaths(String name, MergePathsMode mode) {
     this.name = name;
     this.mode = mode;
   }
@@ -68,29 +65,4 @@
   public String toString() {
     return "MergePaths{" + "mode=" +  mode + '}';
   }
-
-  static class Factory {
-    private Factory() {
-    }
-
-    static MergePaths newInstance(JsonReader reader) throws IOException {
-      String name = null;
-      MergePathsMode mode = null;
-
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "nm":
-            name = reader.nextString();
-            break;
-          case "mm":
-            mode =  MergePathsMode.forId(reader.nextInt());
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-
-      return new MergePaths(name, mode);
-    }
-  }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/PolystarShape.java b/lottie/src/main/java/com/airbnb/lottie/model/content/PolystarShape.java
index 83c979a..640c1d4 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/PolystarShape.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/PolystarShape.java
@@ -1,19 +1,14 @@
 package com.airbnb.lottie.model.content;
 
 import android.graphics.PointF;
-import android.util.JsonReader;
 
-import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
 import com.airbnb.lottie.animation.content.Content;
 import com.airbnb.lottie.animation.content.PolystarContent;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
-import com.airbnb.lottie.model.animatable.AnimatablePathValue;
 import com.airbnb.lottie.model.animatable.AnimatableValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import java.io.IOException;
-
 public class PolystarShape implements ContentModel {
   public enum Type {
     Star(1),
@@ -25,7 +20,7 @@
       this.value = value;
     }
 
-    static Type forValue(int value) {
+    public static Type forValue(int value) {
       for (Type type : Type.values()) {
         if (type.value == value) {
           return type;
@@ -45,7 +40,7 @@
   private final AnimatableFloatValue innerRoundedness;
   private final AnimatableFloatValue outerRoundedness;
 
-  private PolystarShape(String name, Type type, AnimatableFloatValue points,
+  public PolystarShape(String name, Type type, AnimatableFloatValue points,
       AnimatableValue<PointF, PointF> position,
       AnimatableFloatValue rotation, AnimatableFloatValue innerRadius,
       AnimatableFloatValue outerRadius, AnimatableFloatValue innerRoundedness,
@@ -100,60 +95,4 @@
   @Override public Content toContent(LottieDrawable drawable, BaseLayer layer) {
     return new PolystarContent(drawable, layer, this);
   }
-
-  static class Factory {
-    private Factory() {
-    }
-
-    static PolystarShape newInstance(
-        JsonReader reader, LottieComposition composition) throws IOException {
-      String name = null;
-      Type type = null;
-      AnimatableFloatValue points = null;
-      AnimatableValue<PointF, PointF> position = null;
-      AnimatableFloatValue rotation = null;
-      AnimatableFloatValue outerRadius = null;
-      AnimatableFloatValue outerRoundedness = null;
-      AnimatableFloatValue innerRadius = null;
-      AnimatableFloatValue innerRoundedness = null;
-
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "nm":
-            name = reader.nextString();
-            break;
-          case "sy":
-            type = Type.forValue(reader.nextInt());
-            break;
-          case "pt":
-            points = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
-            break;
-          case "p":
-            position = AnimatablePathValue .createAnimatablePathOrSplitDimensionPath(reader, composition);
-            break;
-          case "r":
-            rotation = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
-            break;
-          case "or":
-            outerRadius = AnimatableFloatValue.Factory.newInstance(reader, composition);
-            break;
-          case "os":
-            outerRoundedness = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
-            break;
-          case "ir":
-            innerRadius = AnimatableFloatValue.Factory.newInstance(reader, composition);
-            break;
-          case "is":
-            innerRoundedness = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-
-      return new PolystarShape(
-          name, type, points, position, rotation, innerRadius, outerRadius, innerRoundedness, outerRoundedness);
-    }
-  }
-
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/RectangleShape.java b/lottie/src/main/java/com/airbnb/lottie/model/content/RectangleShape.java
index c7503f9..83a9784 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/RectangleShape.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/RectangleShape.java
@@ -1,27 +1,22 @@
 package com.airbnb.lottie.model.content;
 
 import android.graphics.PointF;
-import android.util.JsonReader;
 
-import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
 import com.airbnb.lottie.animation.content.Content;
 import com.airbnb.lottie.animation.content.RectangleContent;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
-import com.airbnb.lottie.model.animatable.AnimatablePathValue;
 import com.airbnb.lottie.model.animatable.AnimatablePointValue;
 import com.airbnb.lottie.model.animatable.AnimatableValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import java.io.IOException;
-
 public class RectangleShape implements ContentModel {
   private final String name;
   private final AnimatableValue<PointF, PointF> position;
   private final AnimatablePointValue size;
   private final AnimatableFloatValue cornerRadius;
 
-  private RectangleShape(String name, AnimatableValue<PointF, PointF> position,
+  public RectangleShape(String name, AnimatableValue<PointF, PointF> position,
       AnimatablePointValue size, AnimatableFloatValue cornerRadius) {
     this.name = name;
     this.position = position;
@@ -29,41 +24,6 @@
     this.cornerRadius = cornerRadius;
   }
 
-  static class Factory {
-    private Factory() {
-    }
-
-    static RectangleShape newInstance(
-        JsonReader reader, LottieComposition composition) throws IOException {
-      String name = null;
-      AnimatableValue<PointF, PointF> position = null;
-      AnimatablePointValue size = null;
-      AnimatableFloatValue roundedness = null;
-
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "nm":
-            name = reader.nextString();
-            break;
-          case "p":
-            position =
-                AnimatablePathValue.createAnimatablePathOrSplitDimensionPath(reader, composition);
-            break;
-          case "s":
-            size = AnimatablePointValue.Factory.newInstance(reader, composition);
-            break;
-          case "r":
-            roundedness = AnimatableFloatValue.Factory.newInstance(reader, composition);
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-
-      return new RectangleShape(name, position, size, roundedness);
-    }
-  }
-
   public String getName() {
     return name;
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/Repeater.java b/lottie/src/main/java/com/airbnb/lottie/model/content/Repeater.java
index 22f3290..140a5df 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/Repeater.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/Repeater.java
@@ -1,9 +1,7 @@
 package com.airbnb.lottie.model.content;
 
 import android.support.annotation.Nullable;
-import android.util.JsonReader;
 
-import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
 import com.airbnb.lottie.animation.content.Content;
 import com.airbnb.lottie.animation.content.RepeaterContent;
@@ -11,15 +9,13 @@
 import com.airbnb.lottie.model.animatable.AnimatableTransform;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import java.io.IOException;
-
 public class Repeater implements ContentModel {
   private final String name;
   private final AnimatableFloatValue copies;
   private final AnimatableFloatValue offset;
   private final AnimatableTransform transform;
 
-  Repeater(String name, AnimatableFloatValue copies, AnimatableFloatValue offset,
+  public Repeater(String name, AnimatableFloatValue copies, AnimatableFloatValue offset,
       AnimatableTransform transform) {
     this.name = name;
     this.copies = copies;
@@ -46,39 +42,4 @@
   @Nullable @Override public Content toContent(LottieDrawable drawable, BaseLayer layer) {
     return new RepeaterContent(drawable, layer, this);
   }
-
-  final static class Factory {
-
-    private Factory() {
-    }
-
-    static Repeater newInstance(
-        JsonReader reader, LottieComposition composition) throws IOException {
-      String name = null;
-      AnimatableFloatValue copies = null;
-      AnimatableFloatValue offset = null;
-      AnimatableTransform transform = null;
-
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "nm":
-            name = reader.nextString();
-            break;
-          case "c":
-            copies = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
-            break;
-          case "o":
-            offset = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
-            break;
-          case "tr":
-            transform = AnimatableTransform.Factory.newInstance(reader, composition);
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-
-      return new Repeater(name, copies, offset, transform);
-    }
-  }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeFill.java b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeFill.java
index 5b4d892..ecdb17e 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeFill.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeFill.java
@@ -2,9 +2,7 @@
 
 import android.graphics.Path;
 import android.support.annotation.Nullable;
-import android.util.JsonReader;
 
-import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
 import com.airbnb.lottie.animation.content.Content;
 import com.airbnb.lottie.animation.content.FillContent;
@@ -12,8 +10,6 @@
 import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import java.io.IOException;
-
 public class ShapeFill implements ContentModel {
   private final boolean fillEnabled;
   private final Path.FillType fillType;
@@ -21,7 +17,7 @@
   @Nullable private final AnimatableColorValue color;
   @Nullable private final AnimatableIntegerValue opacity;
 
-  private ShapeFill(String name, boolean fillEnabled, Path.FillType fillType,
+  public ShapeFill(String name, boolean fillEnabled, Path.FillType fillType,
       @Nullable AnimatableColorValue color, @Nullable AnimatableIntegerValue opacity) {
     this.name = name;
     this.fillEnabled = fillEnabled;
@@ -30,45 +26,6 @@
     this.opacity = opacity;
   }
 
-  static class Factory {
-    private Factory() {
-    }
-
-    static ShapeFill newInstance(
-        JsonReader reader, LottieComposition composition) throws IOException {
-      AnimatableColorValue color = null;
-      boolean fillEnabled = false;
-      AnimatableIntegerValue opacity = null;
-      String name = null;
-      int fillTypeInt = 1;
-
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "nm":
-            name = reader.nextString();
-            break;
-          case "c":
-            color = AnimatableColorValue.Factory.newInstance(reader, composition);
-            break;
-          case "o":
-            opacity = AnimatableIntegerValue.Factory.newInstance(reader, composition);
-            break;
-          case "fillEnabled":
-            fillEnabled = reader.nextBoolean();
-            break;
-          case "r":
-            fillTypeInt = reader.nextInt();
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-
-      Path.FillType fillType = fillTypeInt == 1 ? Path.FillType.WINDING : Path.FillType.EVEN_ODD;
-      return new ShapeFill(name, fillEnabled, fillType, color, opacity);
-    }
-  }
-
   public String getName() {
     return name;
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeGroup.java b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeGroup.java
index 7701962..dba70a9 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeGroup.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeGroup.java
@@ -1,92 +1,14 @@
 package com.airbnb.lottie.model.content;
 
-import android.support.annotation.Nullable;
-import android.util.JsonReader;
-import android.util.Log;
-
-import com.airbnb.lottie.L;
-import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
 import com.airbnb.lottie.animation.content.Content;
 import com.airbnb.lottie.animation.content.ContentGroup;
-import com.airbnb.lottie.model.animatable.AnimatableTransform;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
 public class ShapeGroup implements ContentModel {
-  @Nullable
-  public static ContentModel shapeItemWithJson(JsonReader reader, LottieComposition composition)
-      throws IOException {
-    String type = null;
-
-    reader.beginObject();
-    while (reader.hasNext()) {
-      if (reader.nextName().equals("ty")) {
-        type = reader.nextString();
-        break;
-      } else {
-        reader.skipValue();
-      }
-    }
-
-    ContentModel model = null;
-    //noinspection ConstantConditions
-    switch (type) {
-      case "gr":
-        model = ShapeGroup.Factory.newInstance(reader, composition);
-        break;
-      case "st":
-        model = ShapeStroke.Factory.newInstance(reader, composition);
-        break;
-      case "gs":
-        model = GradientStroke.Factory.newInstance(reader, composition);
-        break;
-      case "fl":
-        model = ShapeFill.Factory.newInstance(reader, composition);
-        break;
-      case "gf":
-        model = GradientFill.Factory.newInstance(reader, composition);
-        break;
-      case "tr":
-        model = AnimatableTransform.Factory.newInstance(reader, composition);
-        break;
-      case "sh":
-        model = ShapePath.Factory.newInstance(reader, composition);
-        break;
-      case "el":
-        model = CircleShape.Factory.newInstance(reader, composition);
-        break;
-      case "rc":
-        model = RectangleShape.Factory.newInstance(reader, composition);
-        break;
-      case "tm":
-        model = ShapeTrimPath.Factory.newInstance(reader, composition);
-        break;
-      case "sr":
-        model = PolystarShape.Factory.newInstance(reader, composition);
-        break;
-      case "mm":
-        model = MergePaths.Factory.newInstance(reader);
-        break;
-      case "rp":
-        model = Repeater.Factory.newInstance(reader, composition);
-        break;
-      default:
-        Log.w(L.TAG, "Unknown shape type " + type);
-    }
-
-    while (reader.hasNext()) {
-      reader.skipValue();
-    }
-    reader.endObject();
-
-    return model;
-  }
-
   private final String name;
   private final List<ContentModel> items;
 
@@ -95,39 +17,6 @@
     this.items = items;
   }
 
-  static class Factory {
-    private Factory() {
-    }
-
-    private static ShapeGroup newInstance(
-        JsonReader reader, LottieComposition composition) throws IOException {
-      String name = null;
-      List<ContentModel> items = new ArrayList<>();
-
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "nm":
-            name = reader.nextString();
-            break;
-          case "it":
-            reader.beginArray();
-            while (reader.hasNext()) {
-              ContentModel newItem = shapeItemWithJson(reader, composition);
-              if (newItem != null) {
-                items.add(newItem);
-              }
-            }
-            reader.endArray();
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-
-      return new ShapeGroup(name, items);
-    }
-  }
-
   public String getName() {
     return name;
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/ShapePath.java b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapePath.java
index 8b3ea6a..9dd2977 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/ShapePath.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapePath.java
@@ -1,22 +1,17 @@
 package com.airbnb.lottie.model.content;
 
-import android.util.JsonReader;
-
-import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
 import com.airbnb.lottie.animation.content.Content;
 import com.airbnb.lottie.animation.content.ShapeContent;
 import com.airbnb.lottie.model.animatable.AnimatableShapeValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import java.io.IOException;
-
 public class ShapePath implements ContentModel {
   private final String name;
   private final int index;
   private final AnimatableShapeValue shapePath;
 
-  private ShapePath(String name, int index, AnimatableShapeValue shapePath) {
+  public ShapePath(String name, int index, AnimatableShapeValue shapePath) {
     this.name = name;
     this.index = index;
     this.shapePath = shapePath;
@@ -39,34 +34,4 @@
         ", index=" + index +
         '}';
   }
-
-  static class Factory {
-    private Factory() {
-    }
-
-    static ShapePath newInstance(
-        JsonReader reader, LottieComposition composition) throws IOException {
-      String name = null;
-      int ind = 0;
-      AnimatableShapeValue shape = null;
-
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "nm":
-            name = reader.nextString();
-            break;
-          case "ind":
-            ind = reader.nextInt();
-            break;
-          case "ks":
-            shape = AnimatableShapeValue.Factory.newInstance(reader, composition);
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-
-      return new ShapePath(name, ind, shape);
-    }
-  }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeStroke.java b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeStroke.java
index b4d6dfa..0ebb4ee 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeStroke.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeStroke.java
@@ -2,9 +2,7 @@
 
 import android.graphics.Paint;
 import android.support.annotation.Nullable;
-import android.util.JsonReader;
 
-import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
 import com.airbnb.lottie.animation.content.Content;
 import com.airbnb.lottie.animation.content.StrokeContent;
@@ -13,8 +11,6 @@
 import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import java.io.IOException;
-import java.util.ArrayList;
 import java.util.List;
 
 public class ShapeStroke implements ContentModel {
@@ -63,7 +59,7 @@
   private final LineCapType capType;
   private final LineJoinType joinType;
 
-  private ShapeStroke(String name, @Nullable AnimatableFloatValue offset,
+  public ShapeStroke(String name, @Nullable AnimatableFloatValue offset,
       List<AnimatableFloatValue> lineDashPattern, AnimatableColorValue color,
       AnimatableIntegerValue opacity, AnimatableFloatValue width, LineCapType capType,
       LineJoinType joinType) {
@@ -81,90 +77,6 @@
     return new StrokeContent(drawable, layer, this);
   }
 
-  static class Factory {
-    private Factory() {
-    }
-
-    static ShapeStroke newInstance(
-        JsonReader reader, LottieComposition composition) throws IOException {
-      String name = null;
-      AnimatableColorValue color = null;
-      AnimatableFloatValue width = null;
-      AnimatableIntegerValue opacity = null;
-      LineCapType capType = null;
-      LineJoinType joinType = null;
-      AnimatableFloatValue offset = null;
-
-      List<AnimatableFloatValue> lineDashPattern = new ArrayList<>();
-
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "nm":
-            name = reader.nextString();
-            break;
-          case "c":
-            color = AnimatableColorValue.Factory.newInstance(reader, composition);
-            break;
-          case "w":
-            width = AnimatableFloatValue.Factory.newInstance(reader, composition);
-            break;
-          case "o":
-            opacity = AnimatableIntegerValue.Factory.newInstance(reader, composition);
-            break;
-          case "lc":
-            capType = LineCapType.values()[reader.nextInt() - 1];
-            break;
-          case "lj":
-            joinType = LineJoinType.values()[reader.nextInt() - 1];
-            break;
-          case "d":
-            reader.beginArray();
-            while (reader.hasNext()) {
-              String n = null;
-              AnimatableFloatValue val = null;
-
-              reader.beginObject();
-              while (reader.hasNext()) {
-                switch (reader.nextName()) {
-                  case "n":
-                    n = reader.nextString();
-                    break;
-                  case "v":
-                    val = AnimatableFloatValue.Factory.newInstance(reader, composition);
-                    break;
-                  default:
-                    reader.skipValue();
-                }
-              }
-              reader.endObject();
-
-              switch (n) {
-                case "o":
-                  offset = val;
-                  break;
-                case "d":
-                case "g":
-                  lineDashPattern.add(val);
-                  break;
-              }
-            }
-            reader.endArray();
-
-            if (lineDashPattern.size() == 1) {
-              // If there is only 1 value then it is assumed to be equal parts on and off.
-              lineDashPattern.add(lineDashPattern.get(0));
-            }
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-
-      return new ShapeStroke(
-          name, offset, lineDashPattern, color, opacity, width, capType, joinType);
-    }
-  }
-
   public String getName() {
     return name;
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeTrimPath.java b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeTrimPath.java
index c1e834e..43edbfc 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeTrimPath.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeTrimPath.java
@@ -1,23 +1,18 @@
 package com.airbnb.lottie.model.content;
 
-import android.util.JsonReader;
-
-import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
 import com.airbnb.lottie.animation.content.Content;
 import com.airbnb.lottie.animation.content.TrimPathContent;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import java.io.IOException;
-
 public class ShapeTrimPath implements ContentModel {
 
   public enum Type {
     Simultaneously,
     Individually;
 
-    static Type forId(int id) {
+    public static Type forId(int id) {
       switch (id) {
         case 1:
           return Simultaneously;
@@ -35,7 +30,7 @@
   private final AnimatableFloatValue end;
   private final AnimatableFloatValue offset;
 
-  private ShapeTrimPath(String name, Type type, AnimatableFloatValue start,
+  public ShapeTrimPath(String name, Type type, AnimatableFloatValue start,
       AnimatableFloatValue end, AnimatableFloatValue offset) {
     this.name = name;
     this.type = type;
@@ -71,42 +66,4 @@
   @Override public String toString() {
     return "Trim Path: {start: " + start + ", end: " + end + ", offset: " + offset + "}";
   }
-
-  static class Factory {
-    private Factory() {
-    }
-
-    static ShapeTrimPath newInstance(
-        JsonReader reader, LottieComposition composition) throws IOException {
-      String name = null;
-      Type type = null;
-      AnimatableFloatValue start = null;
-      AnimatableFloatValue end = null;
-      AnimatableFloatValue offset = null;
-
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "s":
-            start = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
-            break;
-          case "e":
-            end = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
-            break;
-          case "o":
-            offset = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
-            break;
-          case "nm":
-            name = reader.nextString();
-            break;
-          case "m":
-            type = Type.forId(reader.nextInt());
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-
-      return new ShapeTrimPath(name, type, start, end, offset);
-    }
-  }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/CircleShapeParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/CircleShapeParser.java
new file mode 100644
index 0000000..8165086
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/CircleShapeParser.java
@@ -0,0 +1,48 @@
+package com.airbnb.lottie.parser;
+
+import android.graphics.PointF;
+import android.util.JsonReader;
+
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.model.animatable.AnimatablePathValue;
+import com.airbnb.lottie.model.animatable.AnimatablePointValue;
+import com.airbnb.lottie.model.animatable.AnimatableValue;
+import com.airbnb.lottie.model.content.CircleShape;
+
+import java.io.IOException;
+
+public class CircleShapeParser {
+
+  private CircleShapeParser() {}
+
+  public static CircleShape parse(
+      JsonReader reader, LottieComposition composition) throws IOException {
+    String name = null;
+    AnimatableValue<PointF, PointF> position = null;
+    AnimatablePointValue size = null;
+    boolean reversed = false;
+
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "nm":
+          name = reader.nextString();
+          break;
+        case "p":
+          position = AnimatablePathValue
+              .createAnimatablePathOrSplitDimensionPath(reader, composition);
+          break;
+        case "s":
+          size = AnimatablePointValue.Factory.newInstance(reader, composition);
+          break;
+        case "d":
+          // "d" is 2 for normal and 3 for reversed.
+          reversed = reader.nextInt() == 3;
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+
+    return new CircleShape(name, position, size, reversed);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ContentModelParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ContentModelParser.java
new file mode 100644
index 0000000..d5cbcb1
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ContentModelParser.java
@@ -0,0 +1,89 @@
+package com.airbnb.lottie.parser;
+
+import android.support.annotation.Nullable;
+import android.util.JsonReader;
+import android.util.Log;
+
+import com.airbnb.lottie.L;
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.model.animatable.AnimatableTransform;
+import com.airbnb.lottie.model.content.ContentModel;
+
+import java.io.IOException;
+
+public class ContentModelParser {
+
+  private ContentModelParser() {}
+
+  @Nullable
+  public static ContentModel parse(JsonReader reader, LottieComposition composition)
+      throws IOException {
+    String type = null;
+
+    reader.beginObject();
+    while (reader.hasNext()) {
+      if (reader.nextName().equals("ty")) {
+        type = reader.nextString();
+        break;
+      } else {
+        reader.skipValue();
+      }
+    }
+
+    if (type == null) {
+      return null;
+    }
+
+    ContentModel model = null;
+    switch (type) {
+      case "gr":
+        model = ShapeGroupParser.parse(reader, composition);
+        break;
+      case "st":
+        model = ShapeStrokeParser.parse(reader, composition);
+        break;
+      case "gs":
+        model = GradientStrokeParser.parse(reader, composition);
+        break;
+      case "fl":
+        model = ShapeFillParser.parse(reader, composition);
+        break;
+      case "gf":
+        model = GradientFillParser.parse(reader, composition);
+        break;
+      case "tr":
+        model = AnimatableTransform.Factory.newInstance(reader, composition);
+        break;
+      case "sh":
+        model = ShapePathParser.parse(reader, composition);
+        break;
+      case "el":
+        model = CircleShapeParser.parse(reader, composition);
+        break;
+      case "rc":
+        model = RectangleShapeParser.parse(reader, composition);
+        break;
+      case "tm":
+        model = ShapeTrimPathParser.parse(reader, composition);
+        break;
+      case "sr":
+        model = PolystarShapeParser.parse(reader, composition);
+        break;
+      case "mm":
+        model = MergePathsParser.parse(reader);
+        break;
+      case "rp":
+        model = RepeaterParser.parse(reader, composition);
+        break;
+      default:
+        Log.w(L.TAG, "Unknown shape type " + type);
+    }
+
+    while (reader.hasNext()) {
+      reader.skipValue();
+    }
+    reader.endObject();
+
+    return model;
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/DocumentDataParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/DocumentDataParser.java
new file mode 100644
index 0000000..37b51c8
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/DocumentDataParser.java
@@ -0,0 +1,72 @@
+package com.airbnb.lottie.parser;
+
+import android.util.JsonReader;
+
+import com.airbnb.lottie.model.DocumentData;
+import com.airbnb.lottie.utils.JsonUtils;
+
+import java.io.IOException;
+
+public class DocumentDataParser {
+
+  private DocumentDataParser() {}
+
+  public static DocumentData parse(JsonReader reader) throws IOException {
+    String text = null;
+    String fontName = null;
+    double size = 0;
+    int justification = 0;
+    int tracking = 0;
+    double lineHeight = 0;
+    double baselineShift = 0;
+    int fillColor = 0;
+    int strokeColor = 0;
+    int strokeWidth = 0;
+    boolean strokeOverFill = true;
+
+    reader.beginObject();
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "t":
+          text = reader.nextString();
+          break;
+        case "f":
+          fontName = reader.nextString();
+          break;
+        case "s":
+          size = reader.nextDouble();
+          break;
+        case "j":
+          justification = reader.nextInt();
+          break;
+        case "tr":
+          tracking = reader.nextInt();
+          break;
+        case "lh":
+          lineHeight = reader.nextDouble();
+          break;
+        case "ls":
+          baselineShift = reader.nextDouble();
+          break;
+        case "fc":
+          fillColor = JsonUtils.jsonToColor(reader);
+          break;
+        case "sc":
+          strokeColor = JsonUtils.jsonToColor(reader);
+          break;
+        case "sw":
+          strokeWidth = reader.nextInt();
+          break;
+        case "of":
+          strokeOverFill = reader.nextBoolean();
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+    reader.endObject();
+
+    return new DocumentData(text, fontName, size, justification, tracking, lineHeight,
+        baselineShift, fillColor, strokeColor, strokeWidth, strokeOverFill);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/FontCharacterParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/FontCharacterParser.java
new file mode 100644
index 0000000..f519182
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/FontCharacterParser.java
@@ -0,0 +1,67 @@
+package com.airbnb.lottie.parser;
+
+import android.util.JsonReader;
+
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.model.FontCharacter;
+import com.airbnb.lottie.model.content.ShapeGroup;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FontCharacterParser {
+
+  private FontCharacterParser() {}
+
+  public static FontCharacter parse(
+      JsonReader reader, LottieComposition composition) throws IOException {
+    char character = '\0';
+    int size = 0;
+    double width = 0;
+    String style = null;
+    String fontFamily = null;
+    List<ShapeGroup> shapes = new ArrayList<>();
+
+    reader.beginObject();
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "ch":
+          character = reader.nextString().charAt(0);
+          break;
+        case "size":
+          size = reader.nextInt();
+          break;
+        case "w":
+          width = reader.nextDouble();
+          break;
+        case "style":
+          style = reader.nextString();
+          break;
+        case "fFamily":
+          fontFamily = reader.nextString();
+          break;
+        case "data":
+          reader.beginObject();
+          while (reader.hasNext()) {
+            if ("shapes".equals(reader.nextName())) {
+              reader.beginArray();
+              while (reader.hasNext()) {
+                shapes.add((ShapeGroup) ContentModelParser.parse(reader, composition));
+              }
+              reader.endArray();
+            } else {
+              reader.skipValue();
+            }
+          }
+          reader.endObject();
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+    reader.endObject();
+
+    return new FontCharacter(shapes, character, size, width, style, fontFamily);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/FontParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/FontParser.java
new file mode 100644
index 0000000..44557b4
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/FontParser.java
@@ -0,0 +1,42 @@
+package com.airbnb.lottie.parser;
+
+import android.util.JsonReader;
+
+import com.airbnb.lottie.model.Font;
+
+import java.io.IOException;
+
+public class FontParser {
+
+  private FontParser() {}
+
+  public static Font parse(JsonReader reader) throws IOException {
+    String family = null;
+    String name = null;
+    String style = null;
+    float ascent = 0;
+
+    reader.beginObject();
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "fFamily":
+          family = reader.nextString();
+          break;
+        case "fName":
+          name = reader.nextString();
+          break;
+        case "fStyle":
+          style = reader.nextString();
+          break;
+        case "ascent":
+          ascent = (float) reader.nextDouble();
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+    reader.endObject();
+
+    return new Font(family, name, style, ascent);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/GradientFillParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/GradientFillParser.java
new file mode 100644
index 0000000..3392632
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/GradientFillParser.java
@@ -0,0 +1,75 @@
+package com.airbnb.lottie.parser;
+
+import android.graphics.Path;
+import android.util.JsonReader;
+
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.model.animatable.AnimatableGradientColorValue;
+import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
+import com.airbnb.lottie.model.animatable.AnimatablePointValue;
+import com.airbnb.lottie.model.content.GradientFill;
+import com.airbnb.lottie.model.content.GradientType;
+
+import java.io.IOException;
+
+public class GradientFillParser {
+
+  private GradientFillParser() {}
+
+  public static GradientFill parse(
+      JsonReader reader, LottieComposition composition) throws IOException {
+    String name = null;
+    AnimatableGradientColorValue color = null;
+    AnimatableIntegerValue opacity = null;
+    GradientType gradientType = null;
+    AnimatablePointValue startPoint = null;
+    AnimatablePointValue endPoint = null;
+    Path.FillType fillType = null;
+
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "nm":
+          name = reader.nextString();
+          break;
+        case "g":
+          int points = -1;
+          reader.beginObject();
+          while (reader.hasNext()) {
+            switch (reader.nextName()) {
+              case "p":
+                points = reader.nextInt();
+                break;
+              case "k":
+                color = AnimatableGradientColorValue.Factory
+                    .newInstance(reader, composition, points);
+                break;
+              default:
+                reader.skipValue();
+            }
+          }
+          reader.endObject();
+          break;
+        case "o":
+          opacity = AnimatableIntegerValue.Factory.newInstance(reader, composition);
+          break;
+        case "t":
+          gradientType = reader.nextInt() == 1 ? GradientType.Linear : GradientType.Radial;
+          break;
+        case "s":
+          startPoint = AnimatablePointValue.Factory.newInstance(reader, composition);
+          break;
+        case "e":
+          endPoint = AnimatablePointValue.Factory.newInstance(reader, composition);
+          break;
+        case "r":
+          fillType = reader.nextInt() == 1 ? Path.FillType.WINDING : Path.FillType.EVEN_ODD;
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+
+    return new GradientFill(
+        name, gradientType, fillType, color, opacity, startPoint, endPoint, null, null);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/GradientStrokeParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/GradientStrokeParser.java
new file mode 100644
index 0000000..f612a4c
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/GradientStrokeParser.java
@@ -0,0 +1,123 @@
+package com.airbnb.lottie.parser;
+
+import android.util.JsonReader;
+
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
+import com.airbnb.lottie.model.animatable.AnimatableGradientColorValue;
+import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
+import com.airbnb.lottie.model.animatable.AnimatablePointValue;
+import com.airbnb.lottie.model.content.GradientStroke;
+import com.airbnb.lottie.model.content.GradientType;
+import com.airbnb.lottie.model.content.ShapeStroke;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GradientStrokeParser {
+
+  private GradientStrokeParser() {}
+
+  public static GradientStroke parse(
+      JsonReader reader, LottieComposition composition) throws IOException {
+    String name = null;
+    AnimatableGradientColorValue color = null;
+    AnimatableIntegerValue opacity = null;
+    GradientType gradientType = null;
+    AnimatablePointValue startPoint = null;
+    AnimatablePointValue endPoint = null;
+    AnimatableFloatValue width = null;
+    ShapeStroke.LineCapType capType = null;
+    ShapeStroke.LineJoinType joinType = null;
+    AnimatableFloatValue offset = null;
+
+
+    List<AnimatableFloatValue> lineDashPattern = new ArrayList<>();
+
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "nm":
+          name = reader.nextString();
+          break;
+        case "g":
+          int points = -1;
+          reader.beginObject();
+          while (reader.hasNext()) {
+            switch (reader.nextName()) {
+              case "p":
+                points = reader.nextInt();
+                break;
+              case "k":
+                color = AnimatableGradientColorValue.Factory
+                    .newInstance(reader, composition, points);
+                break;
+              default:
+                reader.skipValue();
+            }
+          }
+          reader.endObject();
+          break;
+        case "o":
+          opacity = AnimatableIntegerValue.Factory.newInstance(reader, composition);
+          break;
+        case "t":
+          gradientType = reader.nextInt() == 1 ? GradientType.Linear : GradientType.Radial;
+          break;
+        case "s":
+          startPoint = AnimatablePointValue.Factory.newInstance(reader, composition);
+          break;
+        case "e":
+          endPoint = AnimatablePointValue.Factory.newInstance(reader, composition);
+          break;
+        case "w":
+          width = AnimatableFloatValue.Factory.newInstance(reader, composition);
+          break;
+        case "lc":
+          capType = ShapeStroke.LineCapType.values()[reader.nextInt() - 1];
+          break;
+        case "lj":
+          joinType = ShapeStroke.LineJoinType.values()[reader.nextInt() - 1];
+          break;
+        case "d":
+          reader.beginArray();
+          while (reader.hasNext()) {
+            String n = null;
+            AnimatableFloatValue val = null;
+            reader.beginObject();
+            while (reader.hasNext()) {
+              switch (reader.nextName()) {
+                case "n":
+                  n = reader.nextString();
+                  break;
+                case "v":
+                  val =  AnimatableFloatValue.Factory.newInstance(reader, composition);
+                  break;
+                default:
+                  reader.skipValue();
+              }
+            }
+            reader.endObject();
+
+            if (n.equals("o")) {
+              offset = val;
+            } else if (n.equals("d") || n.equals("g")) {
+              lineDashPattern.add(val);
+            }
+          }
+          reader.endArray();
+          if (lineDashPattern.size() == 1) {
+            // If there is only 1 value then it is assumed to be equal parts on and off.
+            lineDashPattern.add(lineDashPattern.get(0));
+          }
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+
+    return new GradientStroke(
+        name, gradientType, color, opacity, startPoint, endPoint, width, capType, joinType,
+        lineDashPattern, offset);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java
index 504dbd8..367c3aa 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java
@@ -12,7 +12,6 @@
 import com.airbnb.lottie.model.animatable.AnimatableTransform;
 import com.airbnb.lottie.model.content.ContentModel;
 import com.airbnb.lottie.model.content.Mask;
-import com.airbnb.lottie.model.content.ShapeGroup;
 import com.airbnb.lottie.model.layer.Layer;
 import com.airbnb.lottie.utils.Utils;
 
@@ -23,6 +22,8 @@
 
 public class LayerParser {
 
+  private LayerParser() {}
+
   public static Layer parse(LottieComposition composition) {
     Rect bounds = composition.getBounds();
     return new Layer(
@@ -107,7 +108,7 @@
         case "shapes":
           reader.beginArray();
           while (reader.hasNext()) {
-            ContentModel shape = ShapeGroup.shapeItemWithJson(reader, composition);
+            ContentModel shape = ContentModelParser.parse(reader, composition);
             if (shape != null) {
               shapes.add(shape);
             }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionParser.java
index e44eb34..a3e8539 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionParser.java
@@ -20,6 +20,9 @@
 import java.util.Map;
 
 public class LottieCompositionParser {
+
+  private LottieCompositionParser() {}
+
   public static LottieComposition parse(JsonReader reader) throws IOException {
     float scale = Utils.dpScale();
     float startFrame = 0f;
@@ -180,7 +183,7 @@
         case "list":
           reader.beginArray();
           while (reader.hasNext()) {
-            Font font = Font.Factory.newInstance(reader);
+            Font font = FontParser.parse(reader);
             fonts.put(font.getName(), font);
           }
           reader.endArray();
@@ -197,7 +200,7 @@
       SparseArrayCompat<FontCharacter> characters) throws IOException {
     reader.beginArray();
     while (reader.hasNext()) {
-      FontCharacter character = FontCharacter.Factory.newInstance(reader, composition);
+      FontCharacter character = FontCharacterParser.parse(reader, composition);
       characters.put(character.hashCode(), character);
     }
     reader.endArray();
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/MergePathsParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/MergePathsParser.java
new file mode 100644
index 0000000..2d64700
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/MergePathsParser.java
@@ -0,0 +1,32 @@
+package com.airbnb.lottie.parser;
+
+import android.util.JsonReader;
+
+import com.airbnb.lottie.model.content.MergePaths;
+
+import java.io.IOException;
+
+public class MergePathsParser {
+
+  private MergePathsParser() {}
+
+  public static MergePaths parse(JsonReader reader) throws IOException {
+    String name = null;
+    MergePaths.MergePathsMode mode = null;
+
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "nm":
+          name = reader.nextString();
+          break;
+        case "mm":
+          mode =  MergePaths.MergePathsMode.forId(reader.nextInt());
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+
+    return new MergePaths(name, mode);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/PolystarShapeParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/PolystarShapeParser.java
new file mode 100644
index 0000000..2855346
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/PolystarShapeParser.java
@@ -0,0 +1,68 @@
+package com.airbnb.lottie.parser;
+
+import android.graphics.PointF;
+import android.util.JsonReader;
+
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
+import com.airbnb.lottie.model.animatable.AnimatablePathValue;
+import com.airbnb.lottie.model.animatable.AnimatableValue;
+import com.airbnb.lottie.model.content.PolystarShape;
+
+import java.io.IOException;
+
+public class PolystarShapeParser {
+
+  private PolystarShapeParser() {}
+
+  public static PolystarShape parse(
+      JsonReader reader, LottieComposition composition) throws IOException {
+    String name = null;
+    PolystarShape.Type type = null;
+    AnimatableFloatValue points = null;
+    AnimatableValue<PointF, PointF> position = null;
+    AnimatableFloatValue rotation = null;
+    AnimatableFloatValue outerRadius = null;
+    AnimatableFloatValue outerRoundedness = null;
+    AnimatableFloatValue innerRadius = null;
+    AnimatableFloatValue innerRoundedness = null;
+
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "nm":
+          name = reader.nextString();
+          break;
+        case "sy":
+          type = PolystarShape.Type.forValue(reader.nextInt());
+          break;
+        case "pt":
+          points = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
+          break;
+        case "p":
+          position = AnimatablePathValue
+              .createAnimatablePathOrSplitDimensionPath(reader, composition);
+          break;
+        case "r":
+          rotation = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
+          break;
+        case "or":
+          outerRadius = AnimatableFloatValue.Factory.newInstance(reader, composition);
+          break;
+        case "os":
+          outerRoundedness = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
+          break;
+        case "ir":
+          innerRadius = AnimatableFloatValue.Factory.newInstance(reader, composition);
+          break;
+        case "is":
+          innerRoundedness = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+
+    return new PolystarShape(
+        name, type, points, position, rotation, innerRadius, outerRadius, innerRoundedness, outerRoundedness);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/RectangleShapeParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/RectangleShapeParser.java
new file mode 100644
index 0000000..e077e06
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/RectangleShapeParser.java
@@ -0,0 +1,48 @@
+package com.airbnb.lottie.parser;
+
+import android.graphics.PointF;
+import android.util.JsonReader;
+
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
+import com.airbnb.lottie.model.animatable.AnimatablePathValue;
+import com.airbnb.lottie.model.animatable.AnimatablePointValue;
+import com.airbnb.lottie.model.animatable.AnimatableValue;
+import com.airbnb.lottie.model.content.RectangleShape;
+
+import java.io.IOException;
+
+public class RectangleShapeParser {
+
+  private RectangleShapeParser() {}
+
+  public static RectangleShape parse(
+      JsonReader reader, LottieComposition composition) throws IOException {
+    String name = null;
+    AnimatableValue<PointF, PointF> position = null;
+    AnimatablePointValue size = null;
+    AnimatableFloatValue roundedness = null;
+
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "nm":
+          name = reader.nextString();
+          break;
+        case "p":
+          position =
+              AnimatablePathValue.createAnimatablePathOrSplitDimensionPath(reader, composition);
+          break;
+        case "s":
+          size = AnimatablePointValue.Factory.newInstance(reader, composition);
+          break;
+        case "r":
+          roundedness = AnimatableFloatValue.Factory.newInstance(reader, composition);
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+
+    return new RectangleShape(name, position, size, roundedness);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/RepeaterParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/RepeaterParser.java
new file mode 100644
index 0000000..7664812
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/RepeaterParser.java
@@ -0,0 +1,44 @@
+package com.airbnb.lottie.parser;
+
+import android.util.JsonReader;
+
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
+import com.airbnb.lottie.model.animatable.AnimatableTransform;
+import com.airbnb.lottie.model.content.Repeater;
+
+import java.io.IOException;
+
+public class RepeaterParser {
+
+  private RepeaterParser() {}
+
+  public static Repeater parse(
+      JsonReader reader, LottieComposition composition) throws IOException {
+    String name = null;
+    AnimatableFloatValue copies = null;
+    AnimatableFloatValue offset = null;
+    AnimatableTransform transform = null;
+
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "nm":
+          name = reader.nextString();
+          break;
+        case "c":
+          copies = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
+          break;
+        case "o":
+          offset = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
+          break;
+        case "tr":
+          transform = AnimatableTransform.Factory.newInstance(reader, composition);
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+
+    return new Repeater(name, copies, offset, transform);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeFillParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeFillParser.java
new file mode 100644
index 0000000..dedce47
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeFillParser.java
@@ -0,0 +1,50 @@
+package com.airbnb.lottie.parser;
+
+import android.graphics.Path;
+import android.util.JsonReader;
+
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.model.animatable.AnimatableColorValue;
+import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
+import com.airbnb.lottie.model.content.ShapeFill;
+
+import java.io.IOException;
+
+public class ShapeFillParser {
+
+  private ShapeFillParser() {}
+
+  public static ShapeFill parse(
+      JsonReader reader, LottieComposition composition) throws IOException {
+    AnimatableColorValue color = null;
+    boolean fillEnabled = false;
+    AnimatableIntegerValue opacity = null;
+    String name = null;
+    int fillTypeInt = 1;
+
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "nm":
+          name = reader.nextString();
+          break;
+        case "c":
+          color = AnimatableColorValue.Factory.newInstance(reader, composition);
+          break;
+        case "o":
+          opacity = AnimatableIntegerValue.Factory.newInstance(reader, composition);
+          break;
+        case "fillEnabled":
+          fillEnabled = reader.nextBoolean();
+          break;
+        case "r":
+          fillTypeInt = reader.nextInt();
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+
+    Path.FillType fillType = fillTypeInt == 1 ? Path.FillType.WINDING : Path.FillType.EVEN_ODD;
+    return new ShapeFill(name, fillEnabled, fillType, color, opacity);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeGroupParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeGroupParser.java
new file mode 100644
index 0000000..59528df
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeGroupParser.java
@@ -0,0 +1,44 @@
+package com.airbnb.lottie.parser;
+
+import android.util.JsonReader;
+
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.model.content.ContentModel;
+import com.airbnb.lottie.model.content.ShapeGroup;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ShapeGroupParser {
+
+  private ShapeGroupParser() {}
+
+  public static ShapeGroup parse(
+      JsonReader reader, LottieComposition composition) throws IOException {
+    String name = null;
+    List<ContentModel> items = new ArrayList<>();
+
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "nm":
+          name = reader.nextString();
+          break;
+        case "it":
+          reader.beginArray();
+          while (reader.hasNext()) {
+            ContentModel newItem = ContentModelParser.parse(reader, composition);
+            if (newItem != null) {
+              items.add(newItem);
+            }
+          }
+          reader.endArray();
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+
+    return new ShapeGroup(name, items);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ShapePathParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ShapePathParser.java
new file mode 100644
index 0000000..37b1fca
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ShapePathParser.java
@@ -0,0 +1,39 @@
+package com.airbnb.lottie.parser;
+
+import android.util.JsonReader;
+
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.model.animatable.AnimatableShapeValue;
+import com.airbnb.lottie.model.content.ShapePath;
+
+import java.io.IOException;
+
+public class ShapePathParser {
+
+  private ShapePathParser() {}
+
+  public static ShapePath parse(
+      JsonReader reader, LottieComposition composition) throws IOException {
+    String name = null;
+    int ind = 0;
+    AnimatableShapeValue shape = null;
+
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "nm":
+          name = reader.nextString();
+          break;
+        case "ind":
+          ind = reader.nextInt();
+          break;
+        case "ks":
+          shape = AnimatableShapeValue.Factory.newInstance(reader, composition);
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+
+    return new ShapePath(name, ind, shape);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeStrokeParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeStrokeParser.java
new file mode 100644
index 0000000..46b6cdc
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeStrokeParser.java
@@ -0,0 +1,97 @@
+package com.airbnb.lottie.parser;
+
+import android.util.JsonReader;
+
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.model.animatable.AnimatableColorValue;
+import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
+import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
+import com.airbnb.lottie.model.content.ShapeStroke;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ShapeStrokeParser {
+
+  private ShapeStrokeParser() {}
+
+  public static ShapeStroke parse(
+      JsonReader reader, LottieComposition composition) throws IOException {
+    String name = null;
+    AnimatableColorValue color = null;
+    AnimatableFloatValue width = null;
+    AnimatableIntegerValue opacity = null;
+    ShapeStroke.LineCapType capType = null;
+    ShapeStroke.LineJoinType joinType = null;
+    AnimatableFloatValue offset = null;
+
+    List<AnimatableFloatValue> lineDashPattern = new ArrayList<>();
+
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "nm":
+          name = reader.nextString();
+          break;
+        case "c":
+          color = AnimatableColorValue.Factory.newInstance(reader, composition);
+          break;
+        case "w":
+          width = AnimatableFloatValue.Factory.newInstance(reader, composition);
+          break;
+        case "o":
+          opacity = AnimatableIntegerValue.Factory.newInstance(reader, composition);
+          break;
+        case "lc":
+          capType = ShapeStroke.LineCapType.values()[reader.nextInt() - 1];
+          break;
+        case "lj":
+          joinType = ShapeStroke.LineJoinType.values()[reader.nextInt() - 1];
+          break;
+        case "d":
+          reader.beginArray();
+          while (reader.hasNext()) {
+            String n = null;
+            AnimatableFloatValue val = null;
+
+            reader.beginObject();
+            while (reader.hasNext()) {
+              switch (reader.nextName()) {
+                case "n":
+                  n = reader.nextString();
+                  break;
+                case "v":
+                  val = AnimatableFloatValue.Factory.newInstance(reader, composition);
+                  break;
+                default:
+                  reader.skipValue();
+              }
+            }
+            reader.endObject();
+
+            switch (n) {
+              case "o":
+                offset = val;
+                break;
+              case "d":
+              case "g":
+                lineDashPattern.add(val);
+                break;
+            }
+          }
+          reader.endArray();
+
+          if (lineDashPattern.size() == 1) {
+            // If there is only 1 value then it is assumed to be equal parts on and off.
+            lineDashPattern.add(lineDashPattern.get(0));
+          }
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+
+    return new ShapeStroke(
+        name, offset, lineDashPattern, color, opacity, width, capType, joinType);
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeTrimPathParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeTrimPathParser.java
new file mode 100644
index 0000000..e88a429
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeTrimPathParser.java
@@ -0,0 +1,47 @@
+package com.airbnb.lottie.parser;
+
+import android.util.JsonReader;
+
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
+import com.airbnb.lottie.model.content.ShapeTrimPath;
+
+import java.io.IOException;
+
+public class ShapeTrimPathParser {
+
+  private ShapeTrimPathParser() {}
+
+  public static ShapeTrimPath parse(
+      JsonReader reader, LottieComposition composition) throws IOException {
+    String name = null;
+    ShapeTrimPath.Type type = null;
+    AnimatableFloatValue start = null;
+    AnimatableFloatValue end = null;
+    AnimatableFloatValue offset = null;
+
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "s":
+          start = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
+          break;
+        case "e":
+          end = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
+          break;
+        case "o":
+          offset = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
+          break;
+        case "nm":
+          name = reader.nextString();
+          break;
+        case "m":
+          type = ShapeTrimPath.Type.forId(reader.nextInt());
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+
+    return new ShapeTrimPath(name, type, start, end, offset);
+  }
+}