Use JsonReader for json parsing (#572)

Previously, Lottie used JsonObject for deserialization. That meant that:
1) Deserialization is not guaranteed to be O(n) where n is the size of the json file.
2) The entire json string must be loaded into memory.

Switching to JsonReader has the following advantages:
1) Reading is guaranteed to be O(n).
2) Large files can be read in buffers.

However, deserialization code is much more cumbersome because you can't query for things like the existence of a key, the length of an array, and you can't re-walk the same part of the json multiple times.

@felipecsl Did some work (#137, #139, #145, #152) a year ago to prepare to decouple parsing logic so that people could use jackson or some other method of deserialization. However, JsonReader is now the most optimal solution so some of the factory code can be simplified in a future PR.

## Performance
Most animations deserialize ~50% faster than before.
I was also able to deserialize a 50mb json file in 10s that couldn't come close to completing without OOMing before.

|Animation|Old time (ms)|New time (ms)|% improvement|
|----------|--------------|--------------|----------------|
|Tadah|107|55|48%|
|Nudge|100|51|49%|
|Notifications|85|48|43%|
|Star Wars|74|41|45%|
|City|65|24|64%|
|9squares|17|7|59%|
|Empty State|9|4|56%|
|Hello World|6|2|66%|
|Hamburger Arrow|2|1|50%|

Fixes #39 
  
  
diff --git a/LottieSample/libs/happo.aar b/LottieSample/libs/happo.aar
index d075332..43a9f0a 100644
--- a/LottieSample/libs/happo.aar
+++ b/LottieSample/libs/happo.aar
Binary files differ
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
index 97293c1..396fc3c 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
@@ -9,6 +9,7 @@
 import android.support.annotation.RestrictTo;
 import android.support.v4.util.LongSparseArray;
 import android.support.v4.util.SparseArrayCompat;
+import android.util.JsonReader;
 import android.util.Log;
 
 import com.airbnb.lottie.model.FileCompositionLoader;
@@ -18,14 +19,12 @@
 import com.airbnb.lottie.model.layer.Layer;
 import com.airbnb.lottie.utils.Utils;
 
-import org.json.JSONArray;
-import org.json.JSONException;
 import org.json.JSONObject;
 
-import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -53,30 +52,14 @@
   // This is stored as a set to avoid duplicates.
   private final HashSet<String> warnings = new HashSet<>();
   private final PerformanceTracker performanceTracker = new PerformanceTracker();
-  private final Rect bounds;
-  private final float startFrame;
-  private final float endFrame;
-  private final float frameRate;
-  private final float dpScale;
+  private Rect bounds;
+  private float startFrame;
+  private float endFrame;
+  private float frameRate;
   /* Bodymovin version */
-  private final int majorVersion;
-  private final int minorVersion;
-  private final int patchVersion;
-
-  private LottieComposition(Rect bounds, long startFrame, long endFrame, float frameRate,
-      float dpScale, int major, int minor, int patch) {
-    this.bounds = bounds;
-    this.startFrame = startFrame;
-    this.endFrame = endFrame;
-    this.frameRate = frameRate;
-    this.dpScale = dpScale;
-    this.majorVersion = major;
-    this.minorVersion = minor;
-    this.patchVersion = patch;
-    if (!Utils.isAtLeastVersion(this, 4, 5, 0)) {
-      addWarning("Lottie only supports bodymovin >= 4.5.0");
-    }
-  }
+  private int majorVersion;
+  private int minorVersion;
+  private int patchVersion;
 
   @RestrictTo(RestrictTo.Scope.LIBRARY)
   public void addWarning(String warning) {
@@ -88,7 +71,7 @@
     return new ArrayList<>(Arrays.asList(warnings.toArray(new String[warnings.size()])));
   }
 
-  public void setPerformanceTrackingEnabled(boolean enabled) {
+  @SuppressWarnings("WeakerAccess") public void setPerformanceTrackingEnabled(boolean enabled) {
     performanceTracker.setEnabled(enabled);
   }
 
@@ -166,10 +149,6 @@
   }
 
 
-  public float getDpScale() {
-    return dpScale;
-  }
-
   @Override public String toString() {
     final StringBuilder sb = new StringBuilder("LottieComposition:\n");
     for (Layer layer : layers) {
@@ -199,8 +178,8 @@
     /**
      * Loads a composition from a file stored in res/raw.
      */
-    public static Cancellable fromRawFile(Context context, @RawRes int resId,
-        OnCompositionLoadedListener loadedListener) {
+    @SuppressWarnings("WeakerAccess") public static Cancellable fromRawFile(
+        Context context, @RawRes int resId, OnCompositionLoadedListener loadedListener) {
       return fromInputStream(context, context.getResources().openRawResource(resId), loadedListener);
     }
 
@@ -234,7 +213,7 @@
      */
     public static Cancellable fromJson(Resources res, JSONObject json,
         OnCompositionLoadedListener loadedListener) {
-      JsonCompositionLoader loader = new JsonCompositionLoader(res, loadedListener);
+      JsonCompositionLoader loader = new JsonCompositionLoader(loadedListener);
       loader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, json);
       return loader;
     }
@@ -242,149 +221,199 @@
     @Nullable
     public static LottieComposition fromInputStream(Resources res, InputStream stream) {
       try {
-        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(stream));
-        StringBuilder total = new StringBuilder();
-        String line;
-        while ((line = bufferedReader.readLine()) != null) {
-          total.append(line);
-        }
-        JSONObject jsonObject = new JSONObject(total.toString());
-        return fromJsonSync(res, jsonObject);
+        return fromJsonSync(new JsonReader(new InputStreamReader(stream)));
       } catch (IOException e) {
         Log.e(L.TAG, "Failed to load composition.",
             new IllegalStateException("Unable to find file.", e));
-      } catch (JSONException e) {
-        Log.e(L.TAG, "Failed to load composition.",
-            new IllegalStateException("Unable to load JSON.", e));
       } finally {
         closeQuietly(stream);
       }
       return null;
     }
 
+    /**
+     * Use {@link #fromJsonSync(JsonReader)}
+     */
+    @Deprecated
     public static LottieComposition fromJsonSync(Resources res, JSONObject json) {
-      Rect bounds = null;
-      float scale = res.getDisplayMetrics().density;
-      int width = json.optInt("w", -1);
-      int height = json.optInt("h", -1);
-
-      if (width != -1 && height != -1) {
-        int scaledWidth = (int) (width * scale);
-        int scaledHeight = (int) (height * scale);
-        bounds = new Rect(0, 0, scaledWidth, scaledHeight);
+      try {
+        return fromJsonSync(res, new JsonReader(new StringReader(json.toString())));
+      } catch (IOException e) {
+        throw new IllegalArgumentException("Unable to parse json", e);
       }
+    }
 
-      long startFrame = json.optLong("ip", 0);
-      long endFrame = json.optLong("op", 0);
-      float frameRate = (float) json.optDouble("fr", 0);
-      String version = json.optString("v");
-      String[] versions = version.split("\\.");
-      int major = Integer.parseInt(versions[0]);
-      int minor = Integer.parseInt(versions[1]);
-      int patch = Integer.parseInt(versions[2]);
-      LottieComposition composition = new LottieComposition(
-          bounds, startFrame, endFrame, frameRate, scale, major, minor, patch);
-      JSONArray assetsJson = json.optJSONArray("assets");
-      parseImages(assetsJson, composition);
-      parsePrecomps(assetsJson, composition);
-      parseFonts(json.optJSONObject("fonts"), composition);
-      parseChars(json.optJSONArray("chars"), composition);
-      parseLayers(json, composition);
+    /**
+     * Use {@link #fromJsonSync(JsonReader)}
+     */
+    @Deprecated
+    public static LottieComposition fromJsonSync(
+        @SuppressWarnings("unused") Resources res, JsonReader reader) throws IOException {
+      return fromJsonSync(reader);
+    }
+
+    public static LottieComposition fromJsonSync(JsonReader reader) throws IOException {
+      float scale = Utils.dpScale();
+      int width = -1;
+      LottieComposition composition = new LottieComposition();
+
+      reader.beginObject();
+      while (reader.hasNext()) {
+        switch (reader.nextName()) {
+          case "w":
+            width = reader.nextInt();
+            break;
+          case "h":
+            int height = reader.nextInt();
+            int scaledWidth = (int) (width * scale);
+            int scaledHeight = (int) (height * scale);
+            composition.bounds = new Rect(0, 0, scaledWidth, scaledHeight);
+            break;
+          case "ip":
+            composition.startFrame = (float) reader.nextDouble();
+            break;
+          case "op":
+            composition.endFrame = (float) reader.nextDouble();
+            break;
+          case "fr":
+            composition.frameRate = (float) reader.nextDouble();
+            break;
+          case "v":
+            String version = reader.nextString();
+            String[] versions = version.split("\\.");
+            composition.majorVersion = Integer.parseInt(versions[0]);
+            composition.minorVersion = Integer.parseInt(versions[1]);
+            composition.patchVersion = Integer.parseInt(versions[2]);
+            if (!Utils.isAtLeastVersion(composition, 4, 5, 0)) {
+              composition.addWarning("Lottie only supports bodymovin >= 4.5.0");
+            }
+            break;
+          case "layers":
+            parseLayers(reader, composition);
+            break;
+          case "assets":
+            parseAssets(reader, composition);
+            break;
+          case "fonts":
+            parseFonts(reader, composition);
+            break;
+          case "chars":
+            parseChars(reader, composition);
+            break;
+          default:
+            reader.skipValue();
+        }
+      }
+      reader.endObject();
       return composition;
     }
 
-    private static void parseLayers(JSONObject json, LottieComposition composition) {
-      JSONArray jsonLayers = json.optJSONArray("layers");
-      // This should never be null. Bodymovin always exports at least an empty array.
-      // However, it seems as if the unmarshalling from the React Native library sometimes
-      // causes this to be null. The proper fix should be done there but this will prevent a crash.
-      // https://github.com/airbnb/lottie-android/issues/279
-      if (jsonLayers == null) {
-        return;
-      }
-      int length = jsonLayers.length();
+    private static void parseLayers(JsonReader reader, LottieComposition composition)
+        throws IOException {
       int imageCount = 0;
-      for (int i = 0; i < length; i++) {
-        Layer layer = Layer.Factory.newInstance(jsonLayers.optJSONObject(i), composition);
+      reader.beginArray();
+      while (reader.hasNext()) {
+        Layer layer = Layer.Factory.newInstance(reader, composition);
         if (layer.getLayerType() == Layer.LayerType.Image) {
           imageCount++;
         }
         addLayer(composition.layers, composition.layerMap, layer);
-      }
 
-      if (imageCount > 4) {
-        composition.addWarning("You have " + imageCount + " images. Lottie should primarily be " +
-            "used with shapes. If you are using Adobe Illustrator, convert the Illustrator layers" +
-            " to shape layers.");
+        if (imageCount > 4) {
+          composition.warnings.add("You have " + imageCount + " images. Lottie should primarily be " +
+              "used with shapes. If you are using Adobe Illustrator, convert the Illustrator layers" +
+              " to shape layers.");
+        }
       }
+      reader.endArray();
     }
 
-    private static void parsePrecomps(
-        @Nullable JSONArray assetsJson, LottieComposition composition) {
-      if (assetsJson == null) {
-        return;
-      }
-      int length = assetsJson.length();
-      for (int i = 0; i < length; i++) {
-        JSONObject assetJson = assetsJson.optJSONObject(i);
-        JSONArray layersJson = assetJson.optJSONArray("layers");
-        if (layersJson == null) {
-          continue;
-        }
-        List<Layer> layers = new ArrayList<>(layersJson.length());
+    private static void parseAssets(
+        JsonReader reader, LottieComposition composition) throws IOException {
+      reader.beginArray();
+      while (reader.hasNext()) {
+        String id = null;
+        // For precomps
+        List<Layer> layers = new ArrayList<>();
         LongSparseArray<Layer> layerMap = new LongSparseArray<>();
-        for (int j = 0; j < layersJson.length(); j++) {
-          Layer layer = Layer.Factory.newInstance(layersJson.optJSONObject(j), composition);
-          layerMap.put(layer.getId(), layer);
-          layers.add(layer);
+        // For images
+        int width = 0;
+        int height = 0;
+        String imageFileName = null;
+        String relativeFolder = null;
+        reader.beginObject();
+        while (reader.hasNext()) {
+          switch (reader.nextName()) {
+            case "id":
+              id = reader.nextString();
+              break;
+            case "layers":
+              reader.beginArray();
+              while (reader.hasNext()) {
+                Layer layer = Layer.Factory.newInstance(reader, composition);
+                layerMap.put(layer.getId(), layer);
+                layers.add(layer);
+              }
+              reader.endArray();
+              break;
+            case "w":
+              width = reader.nextInt();
+              break;
+            case "h":
+              height = reader.nextInt();
+              break;
+            case "p":
+              imageFileName = reader.nextString();
+              break;
+            case "u":
+              relativeFolder = reader.nextString();
+              break;
+            default:
+              reader.skipValue();
+          }
         }
-        String id = assetJson.optString("id");
-        composition.precomps.put(id, layers);
-      }
-    }
-
-    private static void parseImages(
-        @Nullable JSONArray assetsJson, LottieComposition composition) {
-      if (assetsJson == null) {
-        return;
-      }
-      int length = assetsJson.length();
-      for (int i = 0; i < length; i++) {
-        JSONObject assetJson = assetsJson.optJSONObject(i);
-        if (!assetJson.has("p")) {
-          continue;
+        reader.endObject();
+        if (!layers.isEmpty()) {
+          composition.precomps.put(id, layers);
+        } else if (imageFileName != null) {
+          LottieImageAsset image =
+              new LottieImageAsset(width, height, id, imageFileName, relativeFolder);
+          composition.images.put(image.getId(), image);
         }
-        LottieImageAsset image = LottieImageAsset.Factory.newInstance(assetJson);
-        composition.images.put(image.getId(), image);
       }
+      reader.endArray();
     }
 
-    private static void parseFonts(@Nullable JSONObject fonts, LottieComposition composition) {
-      if (fonts == null) {
-        return;
+    private static void parseFonts(
+        JsonReader reader, LottieComposition composition) throws IOException {
+
+      reader.beginObject();
+      while (reader.hasNext()) {
+        switch (reader.nextName()) {
+          case "list":
+            reader.beginArray();
+            while (reader.hasNext()) {
+              Font font = Font.Factory.newInstance(reader);
+              composition.fonts.put(font.getName(), font);
+            }
+            reader.endArray();
+            break;
+          default:
+            reader.skipValue();
+        }
       }
-      JSONArray fontsList = fonts.optJSONArray("list");
-      if (fontsList == null) {
-        return;
-      }
-      int length = fontsList.length();
-      for (int i = 0; i < length; i++) {
-        Font font = Font.Factory.newInstance(fontsList.optJSONObject(i));
-        composition.fonts.put(font.getName(), font);
-      }
+      reader.endObject();
     }
 
-    private static void parseChars(@Nullable JSONArray charsJson, LottieComposition composition) {
-      if (charsJson == null) {
-        return;
-      }
-
-      int length = charsJson.length();
-      for (int i = 0; i < length; i++) {
+    private static void parseChars(
+        JsonReader reader, LottieComposition composition) throws IOException {
+      reader.beginArray();
+      while (reader.hasNext()) {
         FontCharacter character =
-            FontCharacter.Factory.newInstance(charsJson.optJSONObject(i), composition);
+            FontCharacter.Factory.newInstance(reader, composition);
         composition.characters.put(character.hashCode(), character);
       }
+      reader.endArray();
     }
 
     private static void addLayer(List<Layer> layers, LongSparseArray<Layer> layerMap, Layer layer) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java b/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java
index 660fb5e..370cef6 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java
@@ -1,7 +1,5 @@
 package com.airbnb.lottie;
 
-import org.json.JSONObject;
-
 /**
  * Data class describing an image asset exported by bodymovin.
  */
@@ -13,7 +11,7 @@
   private final String fileName;
   private final String dirName;
 
-  private LottieImageAsset(int width, int height, String id, String fileName, String dirName) {
+  LottieImageAsset(int width, int height, String id, String fileName, String dirName) {
     this.width = width;
     this.height = height;
     this.id = id;
@@ -21,16 +19,6 @@
     this.dirName = dirName;
   }
 
-  static class Factory {
-    private Factory() {
-    }
-
-    static LottieImageAsset newInstance(JSONObject imageJson) {
-      return new LottieImageAsset(imageJson.optInt("w"), imageJson.optInt("h"), imageJson.optString("id"),
-          imageJson.optString("p"), imageJson.optString("u"));
-    }
-  }
-
   @SuppressWarnings("WeakerAccess") public int getWidth() {
     return width;
   }
@@ -47,7 +35,7 @@
     return fileName;
   }
 
-  public String getDirName() {
+  @SuppressWarnings("unused") public String getDirName() {
     return dirName;
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/Keyframe.java b/lottie/src/main/java/com/airbnb/lottie/animation/Keyframe.java
index f333d21..55007d3 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/Keyframe.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/Keyframe.java
@@ -5,6 +5,8 @@
 import android.support.annotation.Nullable;
 import android.support.v4.util.SparseArrayCompat;
 import android.support.v4.view.animation.PathInterpolatorCompat;
+import android.util.JsonReader;
+import android.util.JsonToken;
 import android.view.animation.Interpolator;
 import android.view.animation.LinearInterpolator;
 
@@ -14,12 +16,9 @@
 import com.airbnb.lottie.utils.MiscUtils;
 import com.airbnb.lottie.utils.Utils;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 public class Keyframe<T> {
@@ -60,6 +59,11 @@
   private float startProgress = Float.MIN_VALUE;
   private float endProgress = Float.MIN_VALUE;
 
+  // Used by PathKeyframe but it has to be parsed by KeyFrame because we use a JsonReader to
+  // deserialzie the data so we have to parse everything in order
+  public PointF pathCp1 = null;
+  public PointF pathCp2 = null;
+
 
   public Keyframe(@SuppressWarnings("NullableProblems") LottieComposition composition,
       @Nullable T startValue, @Nullable T endValue,
@@ -159,86 +163,147 @@
     private Factory() {
     }
 
-    public static <T> Keyframe<T> newInstance(JSONObject json, LottieComposition composition, float scale,
-        AnimatableValue.Factory<T> valueFactory) {
+    public static <T> Keyframe<T> newInstance(JsonReader reader, LottieComposition composition,
+        float scale, AnimatableValue.Factory<T> valueFactory, boolean animated) throws IOException {
+
+      if (animated) {
+        return parseKeyframe(composition, reader, scale, valueFactory);
+      } else {
+        return parseStaticValue(reader, scale, valueFactory);
+      }
+    }
+
+    /**
+     * beginObject will already be called on the keyframe so it can be differentiated with
+     * a non animated value.
+     */
+    private static <T> Keyframe<T> parseKeyframe(LottieComposition composition, JsonReader reader,
+        float scale, AnimatableValue.Factory<T> valueFactory) throws IOException {
       PointF cp1 = null;
       PointF cp2 = null;
       float startFrame = 0;
       T startValue = null;
       T endValue = null;
+      boolean hold = false;
       Interpolator interpolator = null;
 
-      if (json.has("t")) {
-        startFrame = (float) json.optDouble("t", 0);
-        Object startValueJson = json.opt("s");
-        if (startValueJson != null) {
-          startValue = valueFactory.valueFromObject(startValueJson, scale);
+      // Only used by PathKeyframe
+      PointF pathCp1 = null;
+      PointF pathCp2 = null;
+
+      reader.beginObject();
+      while (reader.hasNext()) {
+        switch (reader.nextName()) {
+          case "t":
+            startFrame = (float) reader.nextDouble();
+            break;
+          case "s":
+            startValue = valueFactory.valueFromObject(reader, scale);
+            break;
+          case "e":
+            endValue = valueFactory.valueFromObject(reader, scale);
+            break;
+          case "o":
+            cp1 = JsonUtils.jsonToPoint(reader, scale);
+            break;
+          case "i":
+            cp2 = JsonUtils.jsonToPoint(reader, scale);
+            break;
+          case "h":
+            hold = reader.nextInt() == 1;
+            break;
+          case "to":
+            pathCp1 = JsonUtils.jsonToPoint(reader, scale);
+            break;
+          case "ti":
+            pathCp2 = JsonUtils.jsonToPoint(reader, scale);
+            break;
+          default:
+            reader.skipValue();
         }
-
-        Object endValueJson = json.opt("e");
-        if (endValueJson != null) {
-          endValue = valueFactory.valueFromObject(endValueJson, scale);
-        }
-
-        JSONObject cp1Json = json.optJSONObject("o");
-        JSONObject cp2Json = json.optJSONObject("i");
-        if (cp1Json != null && cp2Json != null) {
-          cp1 = JsonUtils.pointFromJsonObject(cp1Json, scale);
-          cp2 = JsonUtils.pointFromJsonObject(cp2Json, scale);
-        }
-
-        boolean hold = json.optInt("h", 0) == 1;
-
-        if (hold) {
-          endValue = startValue;
-          // TODO: create a HoldInterpolator so progress changes don't invalidate.
-          interpolator = LINEAR_INTERPOLATOR;
-        } else if (cp1 != null) {
-          cp1.x = MiscUtils.clamp(cp1.x, -scale, scale);
-          cp1.y = MiscUtils.clamp(cp1.y, -MAX_CP_VALUE, MAX_CP_VALUE);
-          cp2.x = MiscUtils.clamp(cp2.x, -scale, scale);
-          cp2.y = MiscUtils.clamp(cp2.y, -MAX_CP_VALUE, MAX_CP_VALUE);
-          int hash = Utils.hashFor(cp1.x, cp1.y, cp2.x, cp2.y);
-          WeakReference<Interpolator> interpolatorRef = getInterpolator(hash);
-          if (interpolatorRef != null) {
-            interpolator = interpolatorRef.get();
-          }
-          if (interpolatorRef == null || interpolator == null) {
-            interpolator = PathInterpolatorCompat.create(
-                cp1.x / scale, cp1.y / scale, cp2.x / scale, cp2.y / scale);
-            try {
-              putInterpolator(hash, new WeakReference<>(interpolator));
-            } catch (ArrayIndexOutOfBoundsException e) {
-              // It is not clear why but SparseArrayCompat sometimes fails with this:
-              //     https://github.com/airbnb/lottie-android/issues/452
-              // Because this is not a critical operation, we can safely just ignore it.
-              // I was unable to repro this to attempt a proper fix.
-            }
-          }
-
-        } else {
-          interpolator = LINEAR_INTERPOLATOR;
-        }
-      } else {
-        startValue = valueFactory.valueFromObject(json, scale);
-        endValue = startValue;
       }
-      return new Keyframe<>(composition, startValue, endValue, interpolator, startFrame, null);
+      reader.endObject();
+
+      if (hold) {
+        endValue = startValue;
+        // TODO: create a HoldInterpolator so progress changes don't invalidate.
+        interpolator = LINEAR_INTERPOLATOR;
+      } else if (cp1 != null && cp2 != null) {
+        cp1.x = MiscUtils.clamp(cp1.x, -scale, scale);
+        cp1.y = MiscUtils.clamp(cp1.y, -MAX_CP_VALUE, MAX_CP_VALUE);
+        cp2.x = MiscUtils.clamp(cp2.x, -scale, scale);
+        cp2.y = MiscUtils.clamp(cp2.y, -MAX_CP_VALUE, MAX_CP_VALUE);
+        int hash = Utils.hashFor(cp1.x, cp1.y, cp2.x, cp2.y);
+        WeakReference<Interpolator> interpolatorRef = getInterpolator(hash);
+        if (interpolatorRef != null) {
+          interpolator = interpolatorRef.get();
+        }
+        if (interpolatorRef == null || interpolator == null) {
+          interpolator = PathInterpolatorCompat.create(
+              cp1.x / scale, cp1.y / scale, cp2.x / scale, cp2.y / scale);
+          try {
+            putInterpolator(hash, new WeakReference<>(interpolator));
+          } catch (ArrayIndexOutOfBoundsException e) {
+            // It is not clear why but SparseArrayCompat sometimes fails with this:
+            //     https://github.com/airbnb/lottie-android/issues/452
+            // Because this is not a critical operation, we can safely just ignore it.
+            // I was unable to repro this to attempt a proper fix.
+          }
+        }
+
+      } else {
+        interpolator = LINEAR_INTERPOLATOR;
+      }
+
+      Keyframe<T> keyframe =
+          new Keyframe<>(composition, startValue, endValue, interpolator, startFrame, null);
+      keyframe.pathCp1 = pathCp1;
+      keyframe.pathCp2 = pathCp2;
+      return keyframe;
     }
 
-    public static <T> List<Keyframe<T>> parseKeyframes(JSONArray json,
-        LottieComposition composition,
-        float scale, AnimatableValue.Factory<T> valueFactory) {
-      int length = json.length();
-      if (length == 0) {
-        return Collections.emptyList();
-      }
+    private static <T> Keyframe<T> parseStaticValue(JsonReader reader,
+        float scale, AnimatableValue.Factory<T> valueFactory) throws IOException {
+      T value = valueFactory.valueFromObject(reader, scale);
+      return new Keyframe<>(value);
+    }
+
+    public static <T> List<Keyframe<T>> parseKeyframes(JsonReader reader,
+        LottieComposition composition, float scale, AnimatableValue.Factory<T> valueFactory)
+        throws IOException {
       List<Keyframe<T>> keyframes = new ArrayList<>();
-      for (int i = 0; i < length; i++) {
-        keyframes.add(Keyframe.Factory.newInstance(json.optJSONObject(i), composition, scale,
-            valueFactory));
+
+      if (reader.peek() == JsonToken.STRING) {
+        composition.addWarning("Lottie doesn't support expressions.");
+        return keyframes;
       }
 
+      reader.beginObject();
+      while (reader.hasNext()) {
+        switch (reader.nextName()) {
+          case "k":
+            if (reader.peek() == JsonToken.BEGIN_ARRAY) {
+              reader.beginArray();
+
+              if (reader.peek() == JsonToken.NUMBER) {
+                // For properties in which the static value is an array of numbers.
+                keyframes.add(Keyframe.Factory.newInstance(reader, composition, scale, valueFactory, false));
+              } else {
+                while (reader.hasNext()) {
+                  keyframes.add(Keyframe.Factory.newInstance(reader, composition, scale, valueFactory, true));
+                }
+              }
+              reader.endArray();
+            } else {
+              keyframes.add(Keyframe.Factory.newInstance(reader, composition, scale, valueFactory, false));
+            }
+            break;
+          default:
+            reader.skipValue();
+        }
+      }
+      reader.endObject();
+
       setEndFrames(keyframes);
       return keyframes;
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/BaseKeyframeAnimation.java b/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/BaseKeyframeAnimation.java
index fb4a00f..78e22ec 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/BaseKeyframeAnimation.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/BaseKeyframeAnimation.java
@@ -62,10 +62,6 @@
   }
 
   private Keyframe<K> getCurrentKeyframe() {
-    if (keyframes.isEmpty()) {
-      throw new IllegalStateException("There are no keyframes");
-    }
-
     if (cachedKeyframe != null && cachedKeyframe.containsProgress(progress)) {
       return cachedKeyframe;
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/PathKeyframe.java b/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/PathKeyframe.java
index 93f2b4c..0c72ff3 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/PathKeyframe.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/PathKeyframe.java
@@ -3,55 +3,44 @@
 import android.graphics.Path;
 import android.graphics.PointF;
 import android.support.annotation.Nullable;
-import android.view.animation.Interpolator;
+import android.util.JsonReader;
+import android.util.JsonToken;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.animation.Keyframe;
 import com.airbnb.lottie.model.animatable.AnimatableValue;
-import com.airbnb.lottie.utils.JsonUtils;
 import com.airbnb.lottie.utils.Utils;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
+import java.io.IOException;
 
 public class PathKeyframe extends Keyframe<PointF> {
   @Nullable private Path path;
 
-  private PathKeyframe(LottieComposition composition, @Nullable PointF startValue,
-      @Nullable PointF endValue, @Nullable Interpolator interpolator, float startFrame,
-      @Nullable Float endFrame) {
-    super(composition, startValue, endValue, interpolator, startFrame, endFrame);
+  private PathKeyframe(LottieComposition composition, Keyframe<PointF> keyframe) {
+    super(composition, keyframe.startValue, keyframe.endValue, keyframe.interpolator,
+        keyframe.startFrame, keyframe.endFrame);
+
+    // This must use equals(float, float) because PointF didn't have an equals(PathF) method
+    // until KitKat...
+    boolean equals = endValue != null && startValue != null &&
+        startValue.equals(endValue.x, endValue.y);
+    //noinspection ConstantConditions
+    if (endValue != null && !equals) {
+      path = Utils.createPath(startValue, endValue, keyframe.pathCp1, keyframe.pathCp2);
+    }
   }
 
   public static class Factory {
     private Factory() {
     }
 
-    public static PathKeyframe newInstance(JSONObject json, LottieComposition composition,
-        AnimatableValue.Factory<PointF> valueFactory) {
-      Keyframe<PointF> keyframe = Keyframe.Factory.newInstance(json, composition,
-          composition.getDpScale(), valueFactory);
-      PointF cp1 = null;
-      PointF cp2 = null;
-      JSONArray tiJson = json.optJSONArray("ti");
-      JSONArray toJson = json.optJSONArray("to");
-      if (tiJson != null && toJson != null) {
-        cp1 = JsonUtils.pointFromJsonArray(toJson, composition.getDpScale());
-        cp2 = JsonUtils.pointFromJsonArray(tiJson, composition.getDpScale());
-      }
+    public static PathKeyframe newInstance(JsonReader reader, LottieComposition composition,
+        AnimatableValue.Factory<PointF> valueFactory) throws IOException {
+      boolean animated = reader.peek() == JsonToken.BEGIN_OBJECT;
+      Keyframe<PointF> keyframe = Keyframe.Factory.newInstance(
+          reader, composition, Utils.dpScale(), valueFactory, animated);
 
-      PathKeyframe pathKeyframe = new PathKeyframe(composition, keyframe.startValue,
-          keyframe.endValue, keyframe.interpolator, keyframe.startFrame, keyframe.endFrame);
-
-      // This must use equals(float, float) because PointF didn't have an equals(PathF) method
-      // until KitKat...
-      boolean equals = keyframe.endValue != null && keyframe.startValue != null &&
-          keyframe.startValue.equals(keyframe.endValue.x, keyframe.endValue.y);
-      //noinspection ConstantConditions
-      if (pathKeyframe.endValue != null && !equals) {
-        pathKeyframe.path = Utils.createPath(keyframe.startValue, keyframe.endValue, cp1, cp2);
-      }
-      return pathKeyframe;
+      return new PathKeyframe(composition, keyframe);
     }
   }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/json/JsonKeyframe.java b/lottie/src/main/java/com/airbnb/lottie/json/JsonKeyframe.java
new file mode 100644
index 0000000..7e59f52
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/json/JsonKeyframe.java
@@ -0,0 +1,4 @@
+package com.airbnb.lottie.json;
+
+public class JsonKeyframe {
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/ColorFactory.java b/lottie/src/main/java/com/airbnb/lottie/model/ColorFactory.java
index 18a29a3..2cd6b80 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/ColorFactory.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/ColorFactory.java
@@ -1,32 +1,36 @@
 package com.airbnb.lottie.model;
 
 import android.graphics.Color;
+import android.util.JsonReader;
+import android.util.JsonToken;
 
 import com.airbnb.lottie.model.animatable.AnimatableValue;
 
-import org.json.JSONArray;
+import java.io.IOException;
 
 public class ColorFactory implements AnimatableValue.Factory<Integer> {
   public static final ColorFactory INSTANCE = new ColorFactory();
 
-  @Override public Integer valueFromObject(Object object, float scale) {
-    JSONArray colorArray = (JSONArray) object;
-    if (colorArray.length() == 4) {
-      boolean shouldUse255 = true;
-      for (int i = 0; i < colorArray.length(); i++) {
-        double colorChannel = colorArray.optDouble(i);
-        if (colorChannel > 1f) {
-          shouldUse255 = false;
-        }
-      }
-
-      float multiplier = shouldUse255 ? 255f : 1f;
-      return Color.argb(
-          (int) (colorArray.optDouble(3) * multiplier),
-          (int) (colorArray.optDouble(0) * multiplier),
-          (int) (colorArray.optDouble(1) * multiplier),
-          (int) (colorArray.optDouble(2) * multiplier));
+  @Override public Integer valueFromObject(JsonReader reader, float scale) throws IOException {
+    boolean isArray = reader.peek() == JsonToken.BEGIN_ARRAY;
+    if (isArray) {
+      reader.beginArray();
     }
-    return Color.BLACK;
+    double r = reader.nextDouble();
+    double g = reader.nextDouble();
+    double b = reader.nextDouble();
+    double a = reader.nextDouble();
+    if (isArray) {
+      reader.endArray();
+    }
+
+    if (r <= 1 && g <= 1 && b <= 1 && a <= 1) {
+      r *= 255;
+      g *= 255;
+      b *= 255;
+      a *= 255;
+    }
+
+    return Color.argb((int) a, (int) r, (int) g, (int) b);
   }
 }
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 7cdc953..4e57503 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/DocumentData.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/DocumentData.java
@@ -1,16 +1,17 @@
 package com.airbnb.lottie.model;
 
-import android.graphics.Color;
 import android.support.annotation.ColorInt;
+import android.util.JsonReader;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
+import com.airbnb.lottie.utils.JsonUtils;
+
+import java.io.IOException;
 
 public class DocumentData {
 
   public String text;
   @SuppressWarnings("WeakerAccess") public String fontName;
-  public int size;
+  public double size;
   @SuppressWarnings("WeakerAccess") int justification;
   public int tracking;
   @SuppressWarnings("WeakerAccess") double lineHeight;
@@ -21,7 +22,7 @@
   public boolean strokeOverFill;
 
 
-  DocumentData(String text, String fontName, int size, int justification, int tracking,
+  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;
@@ -43,35 +44,63 @@
     private Factory() {
     }
 
-    public static DocumentData newInstance(JSONObject json) {
-      String text = json.optString("t");
-      String fontName = json.optString("f");
-      int size = json.optInt("s");
-      int justification = json.optInt("j");
-      int tracking = json.optInt("tr");
-      double lineHeight = json.optDouble("lh");
-      double baselineShift = json.optDouble("ls");
-      JSONArray colorArray = json.optJSONArray("fc");
-      int color = Color.argb(
-          255,
-          (int) (colorArray.optDouble(0) * 255),
-          (int) (colorArray.optDouble(1) * 255),
-          (int) (colorArray.optDouble(2) * 255));
-      JSONArray strokeArray = json.optJSONArray("sc");
+    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;
-      if (strokeArray != null) {
-        strokeColor = Color.argb(
-            255,
-            (int) (strokeArray.optDouble(0) * 255),
-            (int) (strokeArray.optDouble(1) * 255),
-            (int) (strokeArray.optDouble(2) * 255));
-      }
+      int strokeWidth = 0;
+      boolean strokeOverFill = true;
 
-      int strokeWidth = json.optInt("sw");
-      boolean strokeOverFill = json.optBoolean("of");
+      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, color, strokeColor, strokeWidth, strokeOverFill);
+          baselineShift, fillColor, strokeColor, strokeWidth, strokeOverFill);
     }
   }
 
@@ -80,7 +109,7 @@
     long temp;
     result = text.hashCode();
     result = 31 * result + fontName.hashCode();
-    result = 31 * result + size;
+    result = (int) (31 * result + size);
     result = 31 * result + justification;
     result = 31 * result + tracking;
     temp = Double.doubleToLongBits(lineHeight);
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 b4a6f97..f1207c2 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/Font.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/Font.java
@@ -1,6 +1,8 @@
 package com.airbnb.lottie.model;
 
-import org.json.JSONObject;
+import android.util.JsonReader;
+
+import java.io.IOException;
 
 public class Font {
 
@@ -34,11 +36,33 @@
 
   public static class Factory {
 
-    public static Font newInstance(JSONObject json) {
-      String family = json.optString("fFamily");
-      String name = json.optString("fName");
-      String style = json.optString("fStyle");
-      float ascent = (float) json.optDouble("ascent");
+    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 17fe401..aa66e81 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/FontCharacter.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/FontCharacter.java
@@ -1,13 +1,12 @@
 package com.airbnb.lottie.model;
 
+import android.util.JsonReader;
+
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.content.ShapeGroup;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 public class FontCharacter {
@@ -55,25 +54,53 @@
 
   public static class Factory {
 
-    public static FontCharacter newInstance(JSONObject json, LottieComposition composition) {
-      char character = json.optString("ch").charAt(0);
-      int size = json.optInt("size");
-      double width = json.optDouble("w");
-      String style = json.optString("style");
-      String fontFamily = json.optString("fFamily");
-      JSONObject data = json.optJSONObject("data");
-      List<ShapeGroup> shapes = Collections.emptyList();
+    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<>();
 
-      if (data != null) {
-        JSONArray shapesJson = data.optJSONArray("shapes");
-        if (shapesJson != null) {
-          shapes = new ArrayList<>(shapesJson.length());
-          for (int i = 0; i < shapesJson.length(); i++) {
-            shapes.add(
-                (ShapeGroup) ShapeGroup.shapeItemWithJson(shapesJson.optJSONObject(i), composition));
-          }
+      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);
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/JsonCompositionLoader.java b/lottie/src/main/java/com/airbnb/lottie/model/JsonCompositionLoader.java
index 512093a..c93dd00 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/JsonCompositionLoader.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/JsonCompositionLoader.java
@@ -1,24 +1,38 @@
 package com.airbnb.lottie.model;
 
 import android.content.res.Resources;
+import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.OnCompositionLoadedListener;
-import com.airbnb.lottie.model.CompositionLoader;
 
 import org.json.JSONObject;
 
+import java.io.IOException;
+import java.io.StringReader;
+
 public final class JsonCompositionLoader extends CompositionLoader<JSONObject> {
-  private final Resources res;
   private final OnCompositionLoadedListener loadedListener;
 
+  /**
+   * Use {@link #JsonCompositionLoader(OnCompositionLoadedListener)}
+   */
+  @Deprecated
   public JsonCompositionLoader(Resources res, OnCompositionLoadedListener loadedListener) {
-    this.res = res;
+    this(loadedListener);
+  }
+
+  @SuppressWarnings("WeakerAccess") public JsonCompositionLoader(OnCompositionLoadedListener loadedListener) {
     this.loadedListener = loadedListener;
   }
 
   @Override protected LottieComposition doInBackground(JSONObject... params) {
-    return LottieComposition.Factory.fromJsonSync(res, params[0]);
+    try {
+      JsonReader reader = new JsonReader(new StringReader(params[0].toString()));
+      return LottieComposition.Factory.fromJsonSync(reader);
+    } catch (IOException e) {
+      throw new IllegalStateException(e);
+    }
   }
 
   @Override protected void onPostExecute(LottieComposition composition) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/PointFFactory.java b/lottie/src/main/java/com/airbnb/lottie/model/PointFFactory.java
index c2995cc..85f63c0 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/PointFFactory.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/PointFFactory.java
@@ -1,12 +1,13 @@
 package com.airbnb.lottie.model;
 
 import android.graphics.PointF;
+import android.util.JsonReader;
+import android.util.JsonToken;
 
 import com.airbnb.lottie.model.animatable.AnimatableValue;
 import com.airbnb.lottie.utils.JsonUtils;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
+import java.io.IOException;
 
 public class PointFFactory implements AnimatableValue.Factory<PointF> {
   public static final PointFFactory INSTANCE = new PointFFactory();
@@ -14,13 +15,23 @@
   private PointFFactory() {
   }
 
-  @Override public PointF valueFromObject(Object object, float scale) {
-    if (object instanceof JSONArray) {
-      return JsonUtils.pointFromJsonArray((JSONArray) object, scale);
-    } else if (object instanceof JSONObject) {
-      return JsonUtils.pointFromJsonObject((JSONObject) object, scale);
+  @Override public PointF valueFromObject(JsonReader reader, float scale) throws IOException {
+    JsonToken token = reader.peek();
+    if (token == JsonToken.BEGIN_ARRAY) {
+      return JsonUtils.jsonToPoint(reader, scale);
+    } else if (token == JsonToken.BEGIN_OBJECT) {
+      return JsonUtils.jsonToPoint(reader, scale);
+    } else if (token == JsonToken.NUMBER) {
+      // This is the case where the static value for a property is an array of numbers.
+      // We begin the array to see if we have an array of keyframes but it's just an array
+      // of static numbers instead.
+      PointF point = new PointF((float) reader.nextDouble() * scale, (float) reader.nextDouble() * scale);
+      while (reader.hasNext()) {
+        reader.skipValue();
+      }
+      return point;
     } else {
-      throw new IllegalArgumentException("Unable to parse point from " + object);
+      throw new IllegalArgumentException("Cannot convert json to point. Next token is " + token);
     }
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableColorValue.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableColorValue.java
index f830df3..0ef8568 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableColorValue.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableColorValue.java
@@ -1,13 +1,14 @@
 package com.airbnb.lottie.model.animatable;
 
+import android.util.JsonReader;
+
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.animation.Keyframe;
 import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
 import com.airbnb.lottie.animation.keyframe.ColorKeyframeAnimation;
 import com.airbnb.lottie.model.ColorFactory;
 
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.util.List;
 
 public class AnimatableColorValue extends BaseAnimatableValue<Integer, Integer> {
@@ -23,9 +24,10 @@
     private Factory() {
     }
 
-    public static AnimatableColorValue newInstance(JSONObject json, LottieComposition composition) {
+    public static AnimatableColorValue newInstance(
+        JsonReader reader, LottieComposition composition) throws IOException {
       return new AnimatableColorValue(
-          AnimatableValueParser.newInstance(json, 1f, composition, ColorFactory.INSTANCE));
+          AnimatableValueParser.newInstance(reader, 1f, composition, ColorFactory.INSTANCE));
     }
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableFloatValue.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableFloatValue.java
index 3a8f3bf..b887665 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableFloatValue.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableFloatValue.java
@@ -1,13 +1,15 @@
 package com.airbnb.lottie.model.animatable;
 
+import android.util.JsonReader;
+
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.animation.Keyframe;
 import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
 import com.airbnb.lottie.animation.keyframe.FloatKeyframeAnimation;
 import com.airbnb.lottie.utils.JsonUtils;
+import com.airbnb.lottie.utils.Utils;
 
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.util.List;
 
 public class AnimatableFloatValue extends BaseAnimatableValue<Float, Float> {
@@ -34,8 +36,8 @@
     private ValueFactory() {
     }
 
-    @Override public Float valueFromObject(Object object, float scale) {
-      return JsonUtils.valueFromObject(object) * scale;
+    @Override public Float valueFromObject(JsonReader reader, float scale) throws IOException {
+      return JsonUtils.valueFromObject(reader) * scale;
     }
   }
 
@@ -47,18 +49,16 @@
       return new AnimatableFloatValue();
     }
 
-    public static AnimatableFloatValue newInstance(JSONObject json, LottieComposition composition) {
-      return newInstance(json, composition, true);
+    public static AnimatableFloatValue newInstance(JsonReader reader, LottieComposition composition)
+        throws IOException {
+      return newInstance(reader, composition, true);
     }
 
     public static AnimatableFloatValue newInstance(
-        JSONObject json, LottieComposition composition, boolean isDp) {
-      float scale = isDp ? composition.getDpScale() : 1f;
-      if (json != null && json.has("x")) {
-        composition.addWarning("Lottie doesn't support expressions.");
-      }
+        JsonReader reader, LottieComposition composition, boolean isDp) throws IOException {
+      float scale = isDp ? Utils.dpScale() : 1f;
       return new AnimatableFloatValue(
-          AnimatableValueParser.newInstance(json, scale, composition, ValueFactory.INSTANCE));
+          AnimatableValueParser.newInstance(reader, scale, composition, ValueFactory.INSTANCE));
     }
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableGradientColorValue.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableGradientColorValue.java
index 9af1860..91d8558 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableGradientColorValue.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableGradientColorValue.java
@@ -2,6 +2,8 @@
 
 import android.graphics.Color;
 import android.support.annotation.IntRange;
+import android.util.JsonReader;
+import android.util.JsonToken;
 import android.util.Log;
 
 import com.airbnb.lottie.L;
@@ -12,9 +14,8 @@
 import com.airbnb.lottie.model.content.GradientColor;
 import com.airbnb.lottie.utils.MiscUtils;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
-
+import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 
 public class AnimatableGradientColorValue extends BaseAnimatableValue<GradientColor,
@@ -33,16 +34,16 @@
     }
 
     public static AnimatableGradientColorValue newInstance(
-        JSONObject json, LottieComposition composition) {
-      int points = json.optInt("p", json.optJSONArray("k").length() / 4);
+        JsonReader reader, LottieComposition composition, int points) throws IOException {
       return new AnimatableGradientColorValue(
-          AnimatableValueParser.newInstance(json, 1, composition, new ValueFactory(points))
+          AnimatableValueParser.newInstance(reader, 1, composition, new ValueFactory(points))
       );
     }
   }
 
   private static class ValueFactory implements AnimatableValue.Factory<GradientColor> {
-    private final int colorPoints;
+    /** The number of colors if it exists in the json or -1 if it doesn't (legacy bodymovin) */
+    private int colorPoints;
 
     private ValueFactory(int colorPoints) {
       this.colorPoints = colorPoints;
@@ -68,22 +69,39 @@
      *     ...
      * ]
      */
-    @Override public GradientColor valueFromObject(Object object, float scale) {
-      JSONArray array = (JSONArray) object;
+    @Override public GradientColor valueFromObject(JsonReader reader, float scale)
+        throws IOException {
+      List<Float> array = new ArrayList<>();
+      // The array was started by Keyframe because it thought that this may be an array of keyframes
+      // but peek returned a number so it considered it a static array of numbers.
+      boolean isArray = reader.peek() == JsonToken.BEGIN_ARRAY;
+      if (isArray) {
+        reader.beginArray();
+      }
+      while (reader.hasNext()) {
+        array.add((float) reader.nextDouble());
+      }
+      if(isArray) {
+        reader.endArray();
+      }
+      if (colorPoints == -1) {
+        colorPoints = array.size() / 4;
+      }
+
       float[] positions = new float[colorPoints];
       int[] colors = new int[colorPoints];
-      GradientColor gradientColor = new GradientColor(positions, colors);
+
       int r = 0;
       int g = 0;
-      if (array.length() != colorPoints * 4) {
-        Log.w(L.TAG, "Unexpected gradient length: " + array.length() +
+      if (array.size() != colorPoints * 4) {
+        Log.w(L.TAG, "Unexpected gradient length: " + array.size() +
             ". Expected " + (colorPoints * 4) + ". This may affect the appearance of the gradient. " +
             "Make sure to save your After Effects file before exporting an animation with " +
             "gradients.");
       }
       for (int i = 0; i < colorPoints * 4; i++) {
         int colorIndex = i / 4;
-        double value = array.optDouble(i);
+        double value = array.get(i);
         switch (i % 4) {
           case 0:
             // position
@@ -102,6 +120,7 @@
         }
       }
 
+      GradientColor gradientColor = new GradientColor(positions, colors);
       addOpacityStopsToGradientIfNeeded(gradientColor, array);
       return gradientColor;
     }
@@ -115,21 +134,21 @@
      * This should be a good approximation is nearly all cases. However, if there are many more
      * opacity stops than color stops, information will be lost.
      */
-    private void addOpacityStopsToGradientIfNeeded(GradientColor gradientColor, JSONArray array) {
+    private void addOpacityStopsToGradientIfNeeded(GradientColor gradientColor, List<Float> array) {
       int startIndex = colorPoints * 4;
-      if (array.length() <= startIndex) {
+      if (array.size() <= startIndex) {
         return;
       }
 
-      int opacityStops = (array.length() - startIndex) / 2;
+      int opacityStops = (array.size() - startIndex) / 2;
       double[] positions = new double[opacityStops];
       double[] opacities = new double[opacityStops];
 
-      for (int i = startIndex, j = 0; i < array.length(); i++) {
+      for (int i = startIndex, j = 0; i < array.size(); i++) {
         if (i % 2 == 0) {
-          positions[j] = array.optDouble(i);
+          positions[j] = array.get(i);
         } else {
-          opacities[j] = array.optDouble(i);
+          opacities[j] = array.get(i);
           j++;
         }
       }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableIntegerValue.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableIntegerValue.java
index 84a5513..642d3a3 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableIntegerValue.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableIntegerValue.java
@@ -1,13 +1,14 @@
 package com.airbnb.lottie.model.animatable;
 
+import android.util.JsonReader;
+
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.animation.Keyframe;
 import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
 import com.airbnb.lottie.animation.keyframe.IntegerKeyframeAnimation;
 import com.airbnb.lottie.utils.JsonUtils;
 
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.util.List;
 
 public class AnimatableIntegerValue extends BaseAnimatableValue<Integer, Integer> {
@@ -16,7 +17,7 @@
     this(100);
   }
 
-  private AnimatableIntegerValue(Integer value) {
+  AnimatableIntegerValue(Integer value) {
     super(value);
   }
 
@@ -37,12 +38,9 @@
     }
 
     public static AnimatableIntegerValue newInstance(
-        JSONObject json, LottieComposition composition) {
-      if (json != null && json.has("x")) {
-        composition.addWarning("Lottie doesn't support expressions.");
-      }
+        JsonReader reader, LottieComposition composition) throws IOException {
       return new AnimatableIntegerValue(
-          AnimatableValueParser.newInstance(json, 1, composition, ValueFactory.INSTANCE)
+          AnimatableValueParser.newInstance(reader, 1, composition, ValueFactory.INSTANCE)
       );
     }
   }
@@ -53,8 +51,8 @@
     private ValueFactory() {
     }
 
-    @Override public Integer valueFromObject(Object object, float scale) {
-      return Math.round(JsonUtils.valueFromObject(object) * scale);
+    @Override public Integer valueFromObject(JsonReader reader, float scale) throws IOException {
+      return Math.round(JsonUtils.valueFromObject(reader) * scale);
     }
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatablePathValue.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatablePathValue.java
index 483a762..5453c29 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatablePathValue.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatablePathValue.java
@@ -1,6 +1,8 @@
 package com.airbnb.lottie.model.animatable;
 
 import android.graphics.PointF;
+import android.util.JsonReader;
+import android.util.JsonToken;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.animation.Keyframe;
@@ -9,23 +11,58 @@
 import com.airbnb.lottie.animation.keyframe.PathKeyframeAnimation;
 import com.airbnb.lottie.animation.keyframe.PointKeyframeAnimation;
 import com.airbnb.lottie.utils.JsonUtils;
+import com.airbnb.lottie.utils.Utils;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
 public class AnimatablePathValue implements AnimatableValue<PointF, PointF> {
   public static AnimatableValue<PointF, PointF> createAnimatablePathOrSplitDimensionPath(
-      JSONObject json, LottieComposition composition) {
-    if (json.has("k")) {
-      return new AnimatablePathValue(json.opt("k"), composition);
-    } else {
-      return new AnimatableSplitDimensionPathValue(
-          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("x"), composition),
-          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("y"), composition));
+      JsonReader reader, LottieComposition composition) throws IOException {
+
+    AnimatablePathValue pathAnimation = null;
+    AnimatableFloatValue xAnimation = null;
+    AnimatableFloatValue yAnimation = null;
+
+    boolean hasExpressions = false;
+
+    reader.beginObject();
+    while (reader.peek() != JsonToken.END_OBJECT) {
+      switch (reader.nextName()) {
+        case "k":
+          pathAnimation = new AnimatablePathValue(reader, composition);
+          break;
+        case "x":
+          if (reader.peek() == JsonToken.STRING) {
+            hasExpressions = true;
+            reader.skipValue();
+          } else {
+            xAnimation = AnimatableFloatValue.Factory.newInstance(reader, composition);
+          }
+          break;
+        case "y":
+          if (reader.peek() == JsonToken.STRING) {
+            hasExpressions = true;
+            reader.skipValue();
+          } else {
+            yAnimation = AnimatableFloatValue.Factory.newInstance(reader, composition);
+          }
+          break;
+        default:
+          reader.skipValue();
+      }
     }
+    reader.endObject();
+
+    if (hasExpressions) {
+      composition.addWarning("Lottie doesn't support expressions.");
+    }
+
+    if (pathAnimation != null) {
+      return pathAnimation;
+    }
+    return new AnimatableSplitDimensionPathValue(xAnimation, yAnimation);
   }
 
   private final List<Keyframe<PointF>> keyframes = new ArrayList<>();
@@ -37,32 +74,21 @@
     keyframes.add(new Keyframe<>(new PointF(0, 0)));
   }
 
-  AnimatablePathValue(Object json, LottieComposition composition) {
-    if (hasKeyframes(json)) {
-      JSONArray jsonArray = (JSONArray) json;
-      int length = jsonArray.length();
-      for (int i = 0; i < length; i++) {
-        JSONObject jsonKeyframe = jsonArray.optJSONObject(i);
-        PathKeyframe keyframe = PathKeyframe.Factory.newInstance(jsonKeyframe, composition,
-            ValueFactory.INSTANCE);
+  AnimatablePathValue(JsonReader reader, LottieComposition composition) throws IOException {
+    if (reader.peek() == JsonToken.BEGIN_ARRAY) {
+      reader.beginArray();
+      while (reader.hasNext()) {
+        PathKeyframe keyframe =
+            PathKeyframe.Factory.newInstance(reader, composition, ValueFactory.INSTANCE);
         keyframes.add(keyframe);
       }
+      reader.endArray();
       Keyframe.setEndFrames(keyframes);
     } else {
-      keyframes.add(
-          new Keyframe<>(JsonUtils.pointFromJsonArray((JSONArray) json, composition.getDpScale())));
+      keyframes.add(new Keyframe<>(JsonUtils.jsonToPoint(reader, Utils.dpScale())));
     }
   }
 
-  private boolean hasKeyframes(Object json) {
-    if (!(json instanceof JSONArray)) {
-      return false;
-    }
-
-    Object firstObject = ((JSONArray) json).opt(0);
-    return firstObject instanceof JSONObject && ((JSONObject) firstObject).has("t");
-  }
-
   @Override
   public BaseKeyframeAnimation<PointF, PointF> createAnimation() {
     if (keyframes.get(0).isStatic()) {
@@ -77,8 +103,8 @@
     private ValueFactory() {
     }
 
-    @Override public PointF valueFromObject(Object object, float scale) {
-      return JsonUtils.pointFromJsonArray((JSONArray) object, scale);
+    @Override public PointF valueFromObject(JsonReader reader, float scale) throws IOException {
+      return JsonUtils.jsonToPoint(reader, scale);
     }
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatablePointValue.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatablePointValue.java
index f984284..e01be61 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatablePointValue.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatablePointValue.java
@@ -1,15 +1,16 @@
 package com.airbnb.lottie.model.animatable;
 
 import android.graphics.PointF;
+import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.animation.Keyframe;
 import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
 import com.airbnb.lottie.animation.keyframe.PointKeyframeAnimation;
 import com.airbnb.lottie.model.PointFFactory;
+import com.airbnb.lottie.utils.Utils;
 
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.util.List;
 
 public class AnimatablePointValue extends BaseAnimatableValue<PointF, PointF> {
@@ -25,10 +26,10 @@
     private Factory() {
     }
 
-    public static AnimatablePointValue newInstance(JSONObject json, LottieComposition composition) {
-      return new AnimatablePointValue(
-          AnimatableValueParser
-              .newInstance(json, composition.getDpScale(), composition, PointFFactory.INSTANCE)
+    public static AnimatablePointValue newInstance(
+        JsonReader reader, LottieComposition composition) throws IOException {
+      return new AnimatablePointValue(AnimatableValueParser
+              .newInstance(reader, Utils.dpScale(), composition, PointFFactory.INSTANCE)
       );
     }
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableScaleValue.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableScaleValue.java
index 6c1b480..322ad2c 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableScaleValue.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableScaleValue.java
@@ -1,13 +1,14 @@
 package com.airbnb.lottie.model.animatable;
 
+import android.util.JsonReader;
+
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.animation.Keyframe;
 import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
 import com.airbnb.lottie.animation.keyframe.ScaleKeyframeAnimation;
 import com.airbnb.lottie.value.ScaleXY;
 
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.util.List;
 
 public class AnimatableScaleValue extends BaseAnimatableValue<ScaleXY, ScaleXY> {
@@ -16,7 +17,7 @@
     this(new ScaleXY(1f, 1f));
   }
 
-  private AnimatableScaleValue(ScaleXY value) {
+  AnimatableScaleValue(ScaleXY value) {
     super(value);
   }
 
@@ -33,9 +34,9 @@
     }
 
     static AnimatableScaleValue newInstance(
-        JSONObject json, LottieComposition composition) {
+        JsonReader reader, LottieComposition composition) throws IOException {
       return new AnimatableScaleValue(
-          AnimatableValueParser.newInstance(json, 1, composition, ScaleXY.Factory.INSTANCE)
+          AnimatableValueParser.newInstance(reader, 1, composition, ScaleXY.Factory.INSTANCE)
       );
     }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableShapeValue.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableShapeValue.java
index 2bb9057..37e5cf4 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableShapeValue.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableShapeValue.java
@@ -1,15 +1,16 @@
 package com.airbnb.lottie.model.animatable;
 
 import android.graphics.Path;
+import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.animation.Keyframe;
 import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
 import com.airbnb.lottie.animation.keyframe.ShapeKeyframeAnimation;
 import com.airbnb.lottie.model.content.ShapeData;
+import com.airbnb.lottie.utils.Utils;
 
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.util.List;
 
 public class AnimatableShapeValue extends BaseAnimatableValue<ShapeData, Path> {
@@ -26,10 +27,10 @@
     private Factory() {
     }
 
-    public static AnimatableShapeValue newInstance(JSONObject json, LottieComposition composition) {
-      return new AnimatableShapeValue(
-          AnimatableValueParser
-              .newInstance(json, composition.getDpScale(), composition, ShapeData.Factory.INSTANCE)
+    public static AnimatableShapeValue newInstance(
+        JsonReader reader, LottieComposition composition) throws IOException {
+      return new AnimatableShapeValue(AnimatableValueParser
+              .newInstance(reader, Utils.dpScale(), composition, ShapeData.Factory.INSTANCE)
       );
     }
   }
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 3922b8a..a1b5ffe 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
@@ -1,12 +1,13 @@
 package com.airbnb.lottie.model.animatable;
 
+import android.util.JsonReader;
+
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.animation.Keyframe;
 import com.airbnb.lottie.animation.keyframe.TextKeyframeAnimation;
 import com.airbnb.lottie.model.DocumentData;
 
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.util.List;
 
 public class AnimatableTextFrame extends BaseAnimatableValue<DocumentData, DocumentData> {
@@ -23,13 +24,11 @@
     private Factory() {
     }
 
-    public static AnimatableTextFrame newInstance(JSONObject json, LottieComposition composition) {
-      if (json != null && json.has("x")) {
-        composition.addWarning("Lottie doesn't support expressions.");
-      }
+    public static AnimatableTextFrame newInstance(
+        JsonReader reader, LottieComposition composition) throws IOException {
       return new AnimatableTextFrame(
           AnimatableValueParser
-              .newInstance(json, 1, composition, AnimatableTextFrame.ValueFactory.INSTANCE));
+              .newInstance(reader, 1, composition, AnimatableTextFrame.ValueFactory.INSTANCE));
     }
   }
 
@@ -40,8 +39,9 @@
     private ValueFactory() {
     }
 
-    @Override public DocumentData valueFromObject(Object object, float scale) {
-      return DocumentData.Factory.newInstance((JSONObject) object);
+    @Override
+    public DocumentData valueFromObject(JsonReader reader, float scale) throws IOException {
+      return DocumentData.Factory.newInstance(reader);
     }
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTextProperties.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTextProperties.java
index 0c3a66e..6ffa4a8 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTextProperties.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTextProperties.java
@@ -1,10 +1,11 @@
 package com.airbnb.lottie.model.animatable;
 
 import android.support.annotation.Nullable;
+import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 
-import org.json.JSONObject;
+import java.io.IOException;
 
 public class AnimatableTextProperties {
 
@@ -27,35 +28,55 @@
     private Factory() {
     }
 
-    public static AnimatableTextProperties newInstance(JSONObject json,
-        LottieComposition composition) {
-      if (json == null || !json.has("a")) {
+    public static AnimatableTextProperties newInstance(
+        JsonReader reader, LottieComposition composition) throws IOException {
+      AnimatableTextProperties anim = null;
+
+      reader.beginObject();
+      while (reader.hasNext()) {
+        switch (reader.nextName()) {
+          case "a":
+            anim = parseAnimatableTextProperties(reader, composition);
+            break;
+          default:
+            reader.skipValue();
+        }
+      }
+      reader.endObject();
+      if (anim == null) {
+        // Not sure if this is possible.
         return new AnimatableTextProperties(null, null, null, null);
       }
-      JSONObject animatablePropertiesJson = json.optJSONObject("a");
-      JSONObject colorJson = animatablePropertiesJson.optJSONObject("fc");
+      return anim;
+    }
+
+    private static AnimatableTextProperties parseAnimatableTextProperties(
+        JsonReader reader, LottieComposition composition) throws IOException {
       AnimatableColorValue color = null;
-      if (colorJson != null) {
-        color = AnimatableColorValue.Factory.newInstance(colorJson, composition);
-      }
-
-      JSONObject strokeJson = animatablePropertiesJson.optJSONObject("sc");
       AnimatableColorValue stroke = null;
-      if (strokeJson != null) {
-        stroke = AnimatableColorValue.Factory.newInstance(strokeJson, composition);
-      }
-
-      JSONObject strokeWidthJson = animatablePropertiesJson.optJSONObject("sw");
       AnimatableFloatValue strokeWidth = null;
-      if (strokeWidthJson != null) {
-        strokeWidth = AnimatableFloatValue.Factory.newInstance(strokeWidthJson, composition);
-      }
-
-      JSONObject trackingJson = animatablePropertiesJson.optJSONObject("t");
       AnimatableFloatValue tracking = null;
-      if (trackingJson != null) {
-        tracking = AnimatableFloatValue.Factory.newInstance(trackingJson, composition);
+
+      reader.beginObject();
+      while (reader.hasNext()) {
+        switch (reader.nextName()) {
+          case "fc":
+            color = AnimatableColorValue.Factory.newInstance(reader, composition);
+            break;
+          case "sc":
+            stroke = AnimatableColorValue.Factory.newInstance(reader, composition);
+            break;
+          case "sw":
+            strokeWidth = AnimatableFloatValue.Factory.newInstance(reader, composition);
+            break;
+          case "t":
+            tracking = AnimatableFloatValue.Factory.newInstance(reader, composition);
+            break;
+          default:
+            reader.skipValue();
+        }
       }
+      reader.endObject();
 
       return new AnimatableTextProperties(color, stroke, strokeWidth, tracking);
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java
index ba53002..5aa5aac 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java
@@ -2,22 +2,21 @@
 
 import android.graphics.PointF;
 import android.support.annotation.Nullable;
+import android.util.JsonReader;
+import android.util.JsonToken;
 import android.util.Log;
 
 import com.airbnb.lottie.L;
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
-import com.airbnb.lottie.animation.Keyframe;
 import com.airbnb.lottie.animation.content.Content;
 import com.airbnb.lottie.animation.content.ModifierContent;
 import com.airbnb.lottie.animation.keyframe.TransformKeyframeAnimation;
-import com.airbnb.lottie.value.ScaleXY;
 import com.airbnb.lottie.model.content.ContentModel;
 import com.airbnb.lottie.model.layer.BaseLayer;
+import com.airbnb.lottie.value.ScaleXY;
 
-import org.json.JSONObject;
-
-import java.util.Collections;
+import java.io.IOException;
 
 public class AnimatableTransform implements ModifierContent, ContentModel {
   private final AnimatablePathValue anchorPoint;
@@ -95,18 +94,65 @@
           endOpacity);
     }
 
-    public static AnimatableTransform newInstance(JSONObject json, LottieComposition composition) {
-      AnimatablePathValue anchorPoint;
+    public static AnimatableTransform newInstance(
+        JsonReader reader, LottieComposition composition) throws IOException {
+      AnimatablePathValue anchorPoint = null;
       AnimatableValue<PointF, PointF> position = null;
-      AnimatableScaleValue scale;
+      AnimatableScaleValue scale = null;
       AnimatableFloatValue rotation = null;
-      AnimatableIntegerValue opacity;
+      AnimatableIntegerValue opacity = null;
       AnimatableFloatValue startOpacity = null;
       AnimatableFloatValue endOpacity = null;
-      JSONObject anchorJson = json.optJSONObject("a");
-      if (anchorJson != null) {
-        anchorPoint = new AnimatablePathValue(anchorJson.opt("k"), composition);
-      } else {
+
+      boolean isObject = reader.peek() == JsonToken.BEGIN_OBJECT;
+      if (isObject) {
+        reader.beginObject();
+      }
+      while (reader.hasNext()) {
+        switch (reader.nextName()) {
+          case "a":
+            reader.beginObject();
+            while (reader.hasNext()) {
+              if (reader.nextName().equals("k")) {
+                anchorPoint = new AnimatablePathValue(reader, composition);
+              } else {
+                reader.skipValue();
+              }
+            }
+            reader.endObject();
+            break;
+          case "p":
+            position =
+                AnimatablePathValue.createAnimatablePathOrSplitDimensionPath(reader, composition);
+            break;
+          case "s":
+            scale = AnimatableScaleValue.Factory.newInstance(reader, composition);
+            break;
+          case "rz":
+            composition.addWarning("Lottie doesn't support 3D layers.");
+          case "r":
+            rotation = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
+            break;
+          case "o":
+            opacity = AnimatableIntegerValue.Factory.newInstance(reader, composition);
+            break;
+          case "so":
+            startOpacity =
+                AnimatableFloatValue.Factory.newInstance(reader, composition, false);
+            break;
+          case "eo":
+            endOpacity =
+                AnimatableFloatValue.Factory.newInstance(reader, composition, false);
+            break;
+          default:
+            reader.skipValue();
+        }
+      }
+      if (isObject) {
+        reader.endObject();
+      }
+
+      if (anchorPoint == null) {
         // Cameras don't have an anchor point property. Although we don't support them, at least
         // we won't crash.
         Log.w(L.TAG, "Layer has no transform property. You may be using an unsupported " +
@@ -114,58 +160,18 @@
         anchorPoint = new AnimatablePathValue();
       }
 
-      JSONObject positionJson = json.optJSONObject("p");
-      if (positionJson != null) {
-        position =
-            AnimatablePathValue.createAnimatablePathOrSplitDimensionPath(positionJson, composition);
-      } else {
-        throwMissingTransform("position");
-      }
-
-      JSONObject scaleJson = json.optJSONObject("s");
-      if (scaleJson != null) {
-        scale = AnimatableScaleValue.Factory.newInstance(scaleJson, composition);
-      } else {
+      if (scale == null) {
         // Somehow some community animations don't have scale in the transform.
-        scale = new AnimatableScaleValue(Collections.<Keyframe<ScaleXY>>emptyList());
+        scale = new AnimatableScaleValue(new ScaleXY(1f, 1f));
       }
 
-      JSONObject rotationJson = json.optJSONObject("r");
-      if (rotationJson == null) {
-        rotationJson = json.optJSONObject("rz");
-      }
-      if (rotationJson != null) {
-        rotation = AnimatableFloatValue.Factory.newInstance(rotationJson, composition, false);
-      } else {
-        throwMissingTransform("rotation");
-      }
-
-      JSONObject opacityJson = json.optJSONObject("o");
-      if (opacityJson != null) {
-        opacity = AnimatableIntegerValue.Factory.newInstance(opacityJson, composition);
-      } else {
+      if (opacity == null) {
         // Repeaters have start/end opacity instead of opacity
-        opacity = new AnimatableIntegerValue(Collections.<Keyframe<Integer>>emptyList());
-      }
-
-      JSONObject startOpacityJson = json.optJSONObject("so");
-      if (startOpacityJson != null) {
-        startOpacity =
-            AnimatableFloatValue.Factory.newInstance(startOpacityJson, composition, false);
-      }
-
-      JSONObject endOpacityJson = json.optJSONObject("eo");
-      if (endOpacityJson != null) {
-        endOpacity =
-            AnimatableFloatValue.Factory.newInstance(endOpacityJson, composition, false);
+        opacity = new AnimatableIntegerValue(100);
       }
 
       return new AnimatableTransform(
           anchorPoint, position, scale, rotation, opacity, startOpacity, endOpacity);
     }
-
-    private static void throwMissingTransform(String missingProperty) {
-      throw new IllegalArgumentException("Missing transform for " + missingProperty);
-    }
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableValue.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableValue.java
index 329b5c3..4da1429 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableValue.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableValue.java
@@ -1,11 +1,15 @@
 package com.airbnb.lottie.model.animatable;
 
+import android.util.JsonReader;
+
 import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
 
+import java.io.IOException;
+
 public interface AnimatableValue<K, A> {
   BaseKeyframeAnimation<K, A> createAnimation();
 
   interface Factory<V> {
-    V valueFromObject(Object object, float scale);
+    V valueFromObject(JsonReader reader, float scale) throws IOException;
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableValueParser.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableValueParser.java
index 95b296c..ae8424a 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableValueParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableValueParser.java
@@ -1,57 +1,42 @@
 package com.airbnb.lottie.model.animatable;
 
 import android.support.annotation.Nullable;
+import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.animation.Keyframe;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import java.util.Collections;
+import java.io.IOException;
 import java.util.List;
 
 class AnimatableValueParser<T> {
-  private final JSONObject json;
+  private final JsonReader reader;
   private final float scale;
   private final LottieComposition composition;
   private final AnimatableValue.Factory<T> valueFactory;
 
-  private AnimatableValueParser(JSONObject json, float scale, LottieComposition
+  private AnimatableValueParser(JsonReader reader, float scale, LottieComposition
       composition, AnimatableValue.Factory<T> valueFactory) {
-    this.json = json;
+    this.reader = reader;
     this.scale = scale;
     this.composition = composition;
     this.valueFactory = valueFactory;
   }
 
-  static <T> List<Keyframe<T>> newInstance(@Nullable JSONObject json, float scale,
-      LottieComposition composition, AnimatableValue.Factory<T> valueFactory) {
+  /**
+   * Will return null if the animation can't be played such as if it has expressions.
+   */
+  @Nullable static <T> List<Keyframe<T>> newInstance(JsonReader reader, float scale,
+      LottieComposition composition, AnimatableValue.Factory<T> valueFactory) throws IOException {
     AnimatableValueParser<T> parser =
-        new AnimatableValueParser<>(json, scale, composition, valueFactory);
+        new AnimatableValueParser<>(reader, scale, composition, valueFactory);
     return parser.parseKeyframes();
   }
 
-  private List<Keyframe<T>> parseKeyframes() {
-    Object k = json.opt("k");
-    if (hasKeyframes(k)) {
-      return Keyframe.Factory.parseKeyframes((JSONArray) k, composition, scale, valueFactory);
-    } else {
-      return parseStaticValue();
-    }
-  }
-
-  private List<Keyframe<T>> parseStaticValue() {
-    T initialValue = valueFactory.valueFromObject(json.opt("k"), scale);
-    return Collections.singletonList(new Keyframe<>(initialValue));
-  }
-
-  private static boolean hasKeyframes(Object json) {
-    if (!(json instanceof JSONArray)) {
-      return false;
-    } else {
-      Object firstObject = ((JSONArray) json).opt(0);
-      return firstObject instanceof JSONObject && ((JSONObject) firstObject).has("t");
-    }
+  /**
+   * Will return null if the animation can't be played such as if it has expressions.
+   */
+  private List<Keyframe<T>> parseKeyframes() throws IOException {
+    return Keyframe.Factory.parseKeyframes(reader, composition, scale, valueFactory);
   }
 }
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 0fba444..f2ff8f0 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,6 +1,7 @@
 package com.airbnb.lottie.model.content;
 
 import android.graphics.PointF;
+import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
@@ -11,7 +12,7 @@
 import com.airbnb.lottie.model.animatable.AnimatableValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import org.json.JSONObject;
+import java.io.IOException;
 
 public class CircleShape implements ContentModel {
   private final String name;
@@ -35,14 +36,35 @@
     private Factory() {
     }
 
-    static CircleShape newInstance(JSONObject json, LottieComposition composition) {
-      return new CircleShape(
-          json.optString("nm"),
-          AnimatablePathValue
-              .createAnimatablePathOrSplitDimensionPath(json.optJSONObject("p"), composition),
-          AnimatablePointValue.Factory.newInstance(json.optJSONObject("s"), composition),
-          // "d" is 2 for normal and 3 for reversed.
-          json.optInt("d", 2) == 3);
+    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);
     }
   }
 
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 0ccc3fa..41ab93d 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,6 +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;
@@ -13,8 +14,7 @@
 import com.airbnb.lottie.model.animatable.AnimatablePointValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import org.json.JSONException;
-import org.json.JSONObject;
+import java.io.IOException;
 
 public class GradientFill implements ContentModel {
 
@@ -88,48 +88,57 @@
     private Factory() {
     }
 
-    static GradientFill newInstance(JSONObject json, LottieComposition composition) {
-      final String name = json.optString("nm");
-
-      JSONObject jsonColor = json.optJSONObject("g");
-      if (jsonColor != null && jsonColor.has("k")) {
-        // This is a hack because the "p" value which contains the number of color points is outside
-        // of "k" which contains the useful data.
-        int points = jsonColor.optInt("p");
-        jsonColor = jsonColor.optJSONObject("k");
-        try {
-          jsonColor.put("p", points);
-        } catch (JSONException e) {
-          // Do nothing. This shouldn't fail.
-        }
-      }
+    static GradientFill newInstance(
+        JsonReader reader, LottieComposition composition) throws IOException {
+      String name = null;
       AnimatableGradientColorValue color = null;
-      if (jsonColor != null) {
-        color = AnimatableGradientColorValue.Factory.newInstance(jsonColor, composition);
-      }
-
-      JSONObject jsonOpacity = json.optJSONObject("o");
       AnimatableIntegerValue opacity = null;
-      if (jsonOpacity != null) {
-        opacity = AnimatableIntegerValue.Factory.newInstance(jsonOpacity, composition);
-      }
-
-      int fillTypeInt = json.optInt("r", 1);
-      Path.FillType fillType = fillTypeInt == 1 ? Path.FillType.WINDING : Path.FillType.EVEN_ODD;
-
-      int gradientTypeInt = json.optInt("t", 1);
-      GradientType gradientType = gradientTypeInt == 1 ? GradientType.Linear : GradientType.Radial;
-
-      JSONObject jsonStartPoint = json.optJSONObject("s");
+      GradientType gradientType = null;
       AnimatablePointValue startPoint = null;
-      if (jsonStartPoint != null) {
-        startPoint = AnimatablePointValue.Factory.newInstance(jsonStartPoint, composition);
-      }
-
-      JSONObject jsonEndPoint = json.optJSONObject("e");
       AnimatablePointValue endPoint = null;
-      if (jsonEndPoint != null) {
-        endPoint = AnimatablePointValue.Factory.newInstance(jsonEndPoint, composition);
+      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,
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 14f786e..5bc70fa 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,6 +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;
@@ -12,9 +13,7 @@
 import com.airbnb.lottie.model.animatable.AnimatablePointValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -103,62 +102,100 @@
     private Factory() {
     }
 
-    static GradientStroke newInstance(JSONObject json, LottieComposition composition) {
-      final String name = json.optString("nm");
-      JSONObject jsonColor = json.optJSONObject("g");
-      if (jsonColor != null && jsonColor.has("k")) {
-        jsonColor = jsonColor.optJSONObject("k");
-      }
+    static GradientStroke newInstance(
+        JsonReader reader, LottieComposition composition) throws IOException {
+      String name = null;
       AnimatableGradientColorValue color = null;
-      if (jsonColor != null) {
-        color = AnimatableGradientColorValue.Factory.newInstance(jsonColor, composition);
-      }
-
-      JSONObject jsonOpacity = json.optJSONObject("o");
       AnimatableIntegerValue opacity = null;
-      if (jsonOpacity != null) {
-        opacity = AnimatableIntegerValue.Factory.newInstance(jsonOpacity, composition);
-      }
-
-      int gradientTypeInt = json.optInt("t", 1);
-      GradientType gradientType = gradientTypeInt == 1 ? GradientType.Linear : GradientType.Radial;
-
-      JSONObject jsonStartPoint = json.optJSONObject("s");
+      GradientType gradientType = null;
       AnimatablePointValue startPoint = null;
-      if (jsonStartPoint != null) {
-        startPoint = AnimatablePointValue.Factory.newInstance(jsonStartPoint, composition);
-      }
-
-      JSONObject jsonEndPoint = json.optJSONObject("e");
       AnimatablePointValue endPoint = null;
-      if (jsonEndPoint != null) {
-        endPoint = AnimatablePointValue.Factory.newInstance(jsonEndPoint, composition);
-      }
-      AnimatableFloatValue width = AnimatableFloatValue.Factory.newInstance(json.optJSONObject("w"),
-          composition);
-
-
-      ShapeStroke.LineCapType capType = ShapeStroke.LineCapType.values()[json.optInt("lc") - 1];
-      ShapeStroke.LineJoinType joinType = ShapeStroke.LineJoinType.values()[json.optInt("lj") - 1];
-
+      AnimatableFloatValue width = null;
+      ShapeStroke.LineCapType capType = null;
+      ShapeStroke.LineJoinType joinType = null;
       AnimatableFloatValue offset = null;
+
+
       List<AnimatableFloatValue> lineDashPattern = new ArrayList<>();
-      if (json.has("d")) {
-        JSONArray dashesJson = json.optJSONArray("d");
-        for (int i = 0; i < dashesJson.length(); i++) {
-          JSONObject dashJson = dashesJson.optJSONObject(i);
-          String n = dashJson.optString("n");
-          if (n.equals("o")) {
-            JSONObject value = dashJson.optJSONObject("v");
-            offset = AnimatableFloatValue.Factory.newInstance(value, composition);
-          } else if (n.equals("d") || n.equals("g")) {
-            JSONObject value = dashJson.optJSONObject("v");
-            lineDashPattern.add(AnimatableFloatValue.Factory.newInstance(value, composition));
-          }
-        }
-        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));
+
+      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();
         }
       }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/Mask.java b/lottie/src/main/java/com/airbnb/lottie/model/content/Mask.java
index d976338..32cb1c5 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/Mask.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/Mask.java
@@ -1,10 +1,12 @@
 package com.airbnb.lottie.model.content;
 
+import android.util.JsonReader;
+
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
 import com.airbnb.lottie.model.animatable.AnimatableShapeValue;
 
-import org.json.JSONObject;
+import java.io.IOException;
 
 public class Mask {
   public enum MaskMode {
@@ -28,27 +30,42 @@
     private Factory() {
     }
 
-    public static Mask newMask(JSONObject json, LottieComposition composition) {
-      MaskMode maskMode;
-      switch (json.optString("mode")) {
-        case "a":
-          maskMode = MaskMode.MaskModeAdd;
-          break;
-        case "s":
-          maskMode = MaskMode.MaskModeSubtract;
-          break;
-        case "i":
-          maskMode = MaskMode.MaskModeIntersect;
-          break;
-        default:
-          maskMode = MaskMode.MaskModeUnknown;
-      }
+    public static Mask newMask(
+        JsonReader reader, LottieComposition composition) throws IOException {
+      MaskMode maskMode = null;
+      AnimatableShapeValue maskPath = null;
+      AnimatableIntegerValue opacity = null;
 
-      AnimatableShapeValue maskPath = AnimatableShapeValue.Factory.newInstance(
-          json.optJSONObject("pt"), composition);
-      JSONObject opacityJson = json.optJSONObject("o");
-      AnimatableIntegerValue opacity =
-          AnimatableIntegerValue.Factory.newInstance(opacityJson, composition);
+      reader.beginObject();
+      while (reader.hasNext()) {
+        switch (reader.nextName()) {
+          case "mode":
+            switch (reader.nextString()) {
+              case "a":
+                maskMode = MaskMode.MaskModeAdd;
+                break;
+              case "s":
+                maskMode = MaskMode.MaskModeSubtract;
+                break;
+              case "i":
+                maskMode = MaskMode.MaskModeIntersect;
+                break;
+              default:
+                maskMode = MaskMode.MaskModeUnknown;
+            }
+            break;
+          case "pt":
+            maskPath = AnimatableShapeValue.Factory.newInstance(reader, composition);
+            break;
+          case "o":
+            opacity = AnimatableIntegerValue.Factory.newInstance(reader, composition);
+            break;
+          default:
+            reader.skipValue();
+        }
+      }
+      reader.endObject();
+
       return new Mask(maskMode, maskPath, opacity);
     }
   }
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 11a595c..fd0c64f 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,6 +1,7 @@
 package com.airbnb.lottie.model.content;
 
 import android.support.annotation.Nullable;
+import android.util.JsonReader;
 import android.util.Log;
 
 import com.airbnb.lottie.L;
@@ -9,7 +10,7 @@
 import com.airbnb.lottie.animation.content.MergePathsContent;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import org.json.JSONObject;
+import java.io.IOException;
 
 
 public class MergePaths implements ContentModel {
@@ -72,8 +73,24 @@
     private Factory() {
     }
 
-    static MergePaths newInstance(JSONObject json) {
-      return new MergePaths(json.optString("nm"), MergePathsMode.forId(json.optInt("mm", 1)));
+    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 c2f86b5..83c979a 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,6 +1,7 @@
 package com.airbnb.lottie.model.content;
 
 import android.graphics.PointF;
+import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
@@ -11,7 +12,7 @@
 import com.airbnb.lottie.model.animatable.AnimatableValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import org.json.JSONObject;
+import java.io.IOException;
 
 public class PolystarShape implements ContentModel {
   public enum Type {
@@ -104,33 +105,54 @@
     private Factory() {
     }
 
-    static PolystarShape newInstance(JSONObject json, LottieComposition composition) {
-      final String name = json.optString("nm");
-      Type type = Type.forValue(json.optInt("sy"));
-      AnimatableFloatValue points =
-          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("pt"), composition, false);
-      AnimatableValue<PointF, PointF> position = AnimatablePathValue
-          .createAnimatablePathOrSplitDimensionPath(json.optJSONObject("p"), composition);
-      AnimatableFloatValue rotation =
-          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("r"), composition, false);
-      AnimatableFloatValue outerRadius =
-          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("or"), composition);
-      AnimatableFloatValue outerRoundedness =
-          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("os"), composition, false);
-      AnimatableFloatValue innerRadius;
-      AnimatableFloatValue innerRoundedness;
+    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;
 
-      if (type == Type.Star) {
-        innerRadius =
-            AnimatableFloatValue.Factory.newInstance(json.optJSONObject("ir"), composition);
-        innerRoundedness =
-            AnimatableFloatValue.Factory.newInstance(json.optJSONObject("is"), composition, false);
-      } else {
-        innerRadius = null;
-        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);
+
+      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 dfe1acf..c7503f9 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,6 +1,7 @@
 package com.airbnb.lottie.model.content;
 
 import android.graphics.PointF;
+import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
@@ -12,7 +13,7 @@
 import com.airbnb.lottie.model.animatable.AnimatableValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import org.json.JSONObject;
+import java.io.IOException;
 
 public class RectangleShape implements ContentModel {
   private final String name;
@@ -32,13 +33,34 @@
     private Factory() {
     }
 
-    static RectangleShape newInstance(JSONObject json, LottieComposition composition) {
-      return new RectangleShape(
-          json.optString("nm"),
-          AnimatablePathValue.createAnimatablePathOrSplitDimensionPath(
-              json.optJSONObject("p"), composition),
-          AnimatablePointValue.Factory.newInstance(json.optJSONObject("s"), composition),
-          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("r"), composition));
+    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);
     }
   }
 
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 47edc26..22f3290 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,6 +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;
@@ -10,7 +11,7 @@
 import com.airbnb.lottie.model.animatable.AnimatableTransform;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import org.json.JSONObject;
+import java.io.IOException;
 
 public class Repeater implements ContentModel {
   private final String name;
@@ -51,14 +52,31 @@
     private Factory() {
     }
 
-    static Repeater newInstance(JSONObject json, LottieComposition composition) {
-      String name = json.optString("nm");
-      AnimatableFloatValue copies =
-          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("c"), composition, false);
-      AnimatableFloatValue offset =
-          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("o"), composition, false);
-      AnimatableTransform transform =
-          AnimatableTransform.Factory.newInstance(json.optJSONObject("tr"), composition);
+    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/ShapeData.java b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeData.java
index 0ca65c3..f09404b 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeData.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeData.java
@@ -2,14 +2,15 @@
 
 import android.graphics.PointF;
 import android.support.annotation.FloatRange;
+import android.util.JsonReader;
+import android.util.JsonToken;
 
 import com.airbnb.lottie.model.CubicCurveData;
 import com.airbnb.lottie.model.animatable.AnimatableValue;
+import com.airbnb.lottie.utils.JsonUtils;
 import com.airbnb.lottie.utils.MiscUtils;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -108,95 +109,77 @@
     private Factory() {
     }
 
-    @Override public ShapeData valueFromObject(Object object, float scale) {
-      JSONObject pointsData = null;
-      if (object instanceof JSONArray) {
-        Object firstObject = ((JSONArray) object).opt(0);
-        if (firstObject instanceof JSONObject && ((JSONObject) firstObject).has("v")) {
-          pointsData = (JSONObject) firstObject;
+    @Override public ShapeData valueFromObject(JsonReader reader, float scale) throws IOException {
+      // Sometimes the points data is in a array of length 1. Sometimes the data is at the top
+      // level.
+      if (reader.peek() == JsonToken.BEGIN_ARRAY) {
+        reader.beginArray();
+      }
+
+      boolean closed = false;
+      List<PointF> pointsArray = null;
+      List<PointF> inTangents = null;
+      List<PointF> outTangents = null;
+      reader.beginObject();
+
+      while (reader.hasNext()) {
+        switch (reader.nextName()) {
+          case "c":
+            closed = reader.nextBoolean();
+            break;
+          case "v":
+            pointsArray =  JsonUtils.jsonToPoints(reader, scale);
+            break;
+          case "i":
+            inTangents =  JsonUtils.jsonToPoints(reader, scale);
+            break;
+          case "o":
+            outTangents =  JsonUtils.jsonToPoints(reader, scale);
+            break;
         }
-      } else if (object instanceof JSONObject && ((JSONObject) object).has("v")) {
-        pointsData = (JSONObject) object;
       }
 
-      if (pointsData == null) {
-        return null;
+      reader.endObject();
+
+      if (reader.peek() == JsonToken.END_ARRAY) {
+        reader.endArray();
       }
 
-      JSONArray pointsArray = pointsData.optJSONArray("v");
-      JSONArray inTangents = pointsData.optJSONArray("i");
-      JSONArray outTangents = pointsData.optJSONArray("o");
-      boolean closed = pointsData.optBoolean("c", false);
+      if (pointsArray == null || inTangents == null || outTangents == null) {
+        throw new IllegalArgumentException("Shape data was missing information.");
+      }
 
-      if (pointsArray == null || inTangents == null || outTangents == null ||
-          pointsArray.length() != inTangents.length() ||
-          pointsArray.length() != outTangents.length()) {
-        throw new IllegalStateException(
-            "Unable to process points array or tangents. " + pointsData);
-      } else if (pointsArray.length() == 0) {
+      if (pointsArray.isEmpty()) {
         return new ShapeData(new PointF(), false, Collections.<CubicCurveData>emptyList());
       }
 
-      int length = pointsArray.length();
-      PointF vertex = vertexAtIndex(0, pointsArray);
-      vertex.x *= scale;
-      vertex.y *= scale;
+      int length = pointsArray.size();
+      PointF vertex = pointsArray.get(0);
       PointF initialPoint = vertex;
       List<CubicCurveData> curves = new ArrayList<>(length);
 
       for (int i = 1; i < length; i++) {
-        vertex = vertexAtIndex(i, pointsArray);
-        PointF previousVertex = vertexAtIndex(i - 1, pointsArray);
-        PointF cp1 = vertexAtIndex(i - 1, outTangents);
-        PointF cp2 = vertexAtIndex(i, inTangents);
+        vertex = pointsArray.get(i);
+        PointF previousVertex = pointsArray.get(i - 1);
+        PointF cp1 = outTangents.get(i - 1);
+        PointF cp2 = inTangents.get(i);
         PointF shapeCp1 = MiscUtils.addPoints(previousVertex, cp1);
         PointF shapeCp2 = MiscUtils.addPoints(vertex, cp2);
-
-        shapeCp1.x *= scale;
-        shapeCp1.y *= scale;
-        shapeCp2.x *= scale;
-        shapeCp2.y *= scale;
-        vertex.x *= scale;
-        vertex.y *= scale;
-
         curves.add(new CubicCurveData(shapeCp1, shapeCp2, vertex));
       }
 
       if (closed) {
-        vertex = vertexAtIndex(0, pointsArray);
-        PointF previousVertex = vertexAtIndex(length - 1, pointsArray);
-        PointF cp1 = vertexAtIndex(length - 1, outTangents);
-        PointF cp2 = vertexAtIndex(0, inTangents);
+        vertex = pointsArray.get(0);
+        PointF previousVertex = pointsArray.get(length - 1);
+        PointF cp1 = outTangents.get(length - 1);
+        PointF cp2 = inTangents.get(0);
 
         PointF shapeCp1 = MiscUtils.addPoints(previousVertex, cp1);
         PointF shapeCp2 = MiscUtils.addPoints(vertex, cp2);
 
-        if (scale != 1f) {
-          shapeCp1.x *= scale;
-          shapeCp1.y *= scale;
-          shapeCp2.x *= scale;
-          shapeCp2.y *= scale;
-          vertex.x *= scale;
-          vertex.y *= scale;
-        }
-
         curves.add(new CubicCurveData(shapeCp1, shapeCp2, vertex));
       }
       return new ShapeData(initialPoint, closed, curves);
     }
-
-    private static PointF vertexAtIndex(int idx, JSONArray points) {
-      if (idx >= points.length()) {
-        throw new IllegalArgumentException(
-            "Invalid index " + idx + ". There are only " + points.length() + " points.");
-      }
-
-      JSONArray pointArray = points.optJSONArray(idx);
-      Object x = pointArray.opt(0);
-      Object y = pointArray.opt(1);
-      return new PointF(
-          x instanceof Double ? ((Double) x).floatValue() : (int) x,
-          y instanceof Double ? ((Double) y).floatValue() : (int) y);
-    }
   }
 }
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 b13ba97..5b4d892 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,6 +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;
@@ -11,7 +12,7 @@
 import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import org.json.JSONObject;
+import java.io.IOException;
 
 public class ShapeFill implements ContentModel {
   private final boolean fillEnabled;
@@ -33,26 +34,37 @@
     private Factory() {
     }
 
-    static ShapeFill newInstance(JSONObject json, LottieComposition composition) {
+    static ShapeFill newInstance(
+        JsonReader reader, LottieComposition composition) throws IOException {
       AnimatableColorValue color = null;
-      boolean fillEnabled;
+      boolean fillEnabled = false;
       AnimatableIntegerValue opacity = null;
-      final String name = json.optString("nm");
+      String name = null;
+      int fillTypeInt = 1;
 
-      JSONObject jsonColor = json.optJSONObject("c");
-      if (jsonColor != null) {
-        color = AnimatableColorValue.Factory.newInstance(jsonColor, composition);
+      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();
+        }
       }
 
-      JSONObject jsonOpacity = json.optJSONObject("o");
-      if (jsonOpacity != null) {
-        opacity = AnimatableIntegerValue.Factory.newInstance(jsonOpacity, composition);
-      }
-      fillEnabled = json.optBoolean("fillEnabled");
-
-      int fillTypeInt = json.optInt("r", 1);
       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/model/content/ShapeGroup.java b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeGroup.java
index d2827b7..7701962 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,6 +1,7 @@
 package com.airbnb.lottie.model.content;
 
 import android.support.annotation.Nullable;
+import android.util.JsonReader;
 import android.util.Log;
 
 import com.airbnb.lottie.L;
@@ -11,49 +12,79 @@
 import com.airbnb.lottie.model.animatable.AnimatableTransform;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
-
+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(JSONObject json, LottieComposition composition) {
-    String type = json.optString("ty");
+  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":
-        return ShapeGroup.Factory.newInstance(json, composition);
+        model = ShapeGroup.Factory.newInstance(reader, composition);
+        break;
       case "st":
-        return ShapeStroke.Factory.newInstance(json, composition);
+        model = ShapeStroke.Factory.newInstance(reader, composition);
+        break;
       case "gs":
-        return GradientStroke.Factory.newInstance(json, composition);
+        model = GradientStroke.Factory.newInstance(reader, composition);
+        break;
       case "fl":
-        return ShapeFill.Factory.newInstance(json, composition);
+        model = ShapeFill.Factory.newInstance(reader, composition);
+        break;
       case "gf":
-        return GradientFill.Factory.newInstance(json, composition);
+        model = GradientFill.Factory.newInstance(reader, composition);
+        break;
       case "tr":
-        return AnimatableTransform.Factory.newInstance(json, composition);
+        model = AnimatableTransform.Factory.newInstance(reader, composition);
+        break;
       case "sh":
-        return ShapePath.Factory.newInstance(json, composition);
+        model = ShapePath.Factory.newInstance(reader, composition);
+        break;
       case "el":
-        return CircleShape.Factory.newInstance(json, composition);
+        model = CircleShape.Factory.newInstance(reader, composition);
+        break;
       case "rc":
-        return RectangleShape.Factory.newInstance(json, composition);
+        model = RectangleShape.Factory.newInstance(reader, composition);
+        break;
       case "tm":
-        return ShapeTrimPath.Factory.newInstance(json, composition);
+        model = ShapeTrimPath.Factory.newInstance(reader, composition);
+        break;
       case "sr":
-        return PolystarShape.Factory.newInstance(json, composition);
+        model = PolystarShape.Factory.newInstance(reader, composition);
+        break;
       case "mm":
-        return MergePaths.Factory.newInstance(json);
+        model = MergePaths.Factory.newInstance(reader);
+        break;
       case "rp":
-        return Repeater.Factory.newInstance(json, composition);
+        model = Repeater.Factory.newInstance(reader, composition);
+        break;
       default:
         Log.w(L.TAG, "Unknown shape type " + type);
     }
-    return null;
+
+    while (reader.hasNext()) {
+      reader.skipValue();
+    }
+    reader.endObject();
+
+    return model;
   }
 
   private final String name;
@@ -68,17 +99,31 @@
     private Factory() {
     }
 
-    private static ShapeGroup newInstance(JSONObject json, LottieComposition composition) {
-      JSONArray jsonItems = json.optJSONArray("it");
-      String name = json.optString("nm");
+    private static ShapeGroup newInstance(
+        JsonReader reader, LottieComposition composition) throws IOException {
+      String name = null;
       List<ContentModel> items = new ArrayList<>();
 
-      for (int i = 0; i < jsonItems.length(); i++) {
-        ContentModel newItem = shapeItemWithJson(jsonItems.optJSONObject(i), composition);
-        if (newItem != null) {
-          items.add(newItem);
+      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);
     }
   }
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 a1ec58d..8b3ea6a 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,5 +1,7 @@
 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;
@@ -7,7 +9,7 @@
 import com.airbnb.lottie.model.animatable.AnimatableShapeValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import org.json.JSONObject;
+import java.io.IOException;
 
 public class ShapePath implements ContentModel {
   private final String name;
@@ -42,10 +44,29 @@
     private Factory() {
     }
 
-    static ShapePath newInstance(JSONObject json, LottieComposition composition) {
-      AnimatableShapeValue animatableShapeValue =
-          AnimatableShapeValue.Factory.newInstance(json.optJSONObject("ks"), composition);
-      return new ShapePath(json.optString("nm"), json.optInt("ind"), animatableShapeValue);
+    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 1712aee..b4d6dfa 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,6 +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;
@@ -12,9 +13,7 @@
 import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -86,39 +85,83 @@
     private Factory() {
     }
 
-    static ShapeStroke newInstance(JSONObject json, LottieComposition composition) {
-      final String name = json.optString("nm");
-      List<AnimatableFloatValue> lineDashPattern = new ArrayList<>();
-      AnimatableColorValue color = AnimatableColorValue.Factory.newInstance(json.optJSONObject("c"),
-          composition);
-      AnimatableFloatValue width = AnimatableFloatValue.Factory.newInstance(json.optJSONObject("w"),
-          composition);
-      AnimatableIntegerValue opacity = AnimatableIntegerValue.Factory.newInstance(
-          json.optJSONObject("o"), composition);
-      LineCapType capType = LineCapType.values()[json.optInt("lc") - 1];
-      LineJoinType joinType = LineJoinType.values()[json.optInt("lj") - 1];
+    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;
 
-      if (json.has("d")) {
-        JSONArray dashesJson = json.optJSONArray("d");
-        for (int i = 0; i < dashesJson.length(); i++) {
-          JSONObject dashJson = dashesJson.optJSONObject(i);
-          String n = dashJson.optString("n");
-          if (n.equals("o")) {
-            JSONObject value = dashJson.optJSONObject("v");
-            offset = AnimatableFloatValue.Factory.newInstance(value, composition);
-          } else if (n.equals("d") || n.equals("g")) {
-            JSONObject value = dashJson.optJSONObject("v");
-            lineDashPattern.add(AnimatableFloatValue.Factory.newInstance(value, composition));
-          }
-        }
-        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));
+      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);
+
+      return new ShapeStroke(
+          name, offset, lineDashPattern, color, opacity, width, capType, joinType);
     }
   }
 
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 785ee32..c1e834e 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,5 +1,7 @@
 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;
@@ -7,7 +9,7 @@
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
 import com.airbnb.lottie.model.layer.BaseLayer;
 
-import org.json.JSONObject;
+import java.io.IOException;
 
 public class ShapeTrimPath implements ContentModel {
 
@@ -74,13 +76,37 @@
     private Factory() {
     }
 
-    static ShapeTrimPath newInstance(JSONObject json, LottieComposition composition) {
-      return new ShapeTrimPath(
-          json.optString("nm"),
-          Type.forId(json.optInt("m", 1)),
-          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("s"), composition, false),
-          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("e"), composition, false),
-          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("o"), composition, false));
+    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/model/layer/BaseLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
index 4976df4..111ed3a 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
@@ -25,6 +25,7 @@
 import com.airbnb.lottie.model.KeyPath;
 import com.airbnb.lottie.model.KeyPathElement;
 import com.airbnb.lottie.model.content.Mask;
+import com.airbnb.lottie.utils.Utils;
 import com.airbnb.lottie.value.LottieValueCallback;
 
 import java.util.ArrayList;
@@ -48,7 +49,7 @@
       case Solid:
         return new SolidLayer(drawable, layerModel);
       case Image:
-        return new ImageLayer(drawable, layerModel, composition.getDpScale());
+        return new ImageLayer(drawable, layerModel);
       case Null:
         return new NullLayer(drawable, layerModel);
       case Text:
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/CompositionLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/CompositionLayer.java
index ed6af1a..9cf381b 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/CompositionLayer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/CompositionLayer.java
@@ -37,6 +37,7 @@
     if (timeRemapping != null) {
       this.timeRemapping = timeRemapping.createAnimation();
       addAnimation(this.timeRemapping);
+      //noinspection ConstantConditions
       this.timeRemapping.addUpdateListener(this);
     } else {
       this.timeRemapping = null;
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java
index 46e3222..6e8f1ba 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java
@@ -14,6 +14,7 @@
 import com.airbnb.lottie.LottieProperty;
 import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
 import com.airbnb.lottie.animation.keyframe.ValueCallbackKeyframeAnimation;
+import com.airbnb.lottie.utils.Utils;
 import com.airbnb.lottie.value.LottieValueCallback;
 
 public class ImageLayer extends BaseLayer {
@@ -21,12 +22,10 @@
   private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
   private final Rect src = new Rect();
   private final Rect dst = new Rect();
-  private final float density;
   @Nullable private BaseKeyframeAnimation<ColorFilter, ColorFilter> colorFilterAnimation;
 
-  ImageLayer(LottieDrawable lottieDrawable, Layer layerModel, float density) {
+  ImageLayer(LottieDrawable lottieDrawable, Layer layerModel) {
     super(lottieDrawable, layerModel);
-    this.density = density;
   }
 
   @Override public void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) {
@@ -34,6 +33,8 @@
     if (bitmap == null) {
       return;
     }
+    float density = Utils.dpScale();
+
     paint.setAlpha(parentAlpha);
     if (colorFilterAnimation != null) {
       paint.setColorFilter(colorFilterAnimation.getValue());
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/Layer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/Layer.java
index 446ab56..65cca14 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/Layer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/Layer.java
@@ -3,9 +3,8 @@
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.support.annotation.Nullable;
-import android.util.Log;
+import android.util.JsonReader;
 
-import com.airbnb.lottie.L;
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.animation.Keyframe;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
@@ -17,17 +16,13 @@
 import com.airbnb.lottie.model.content.ShapeGroup;
 import com.airbnb.lottie.utils.Utils;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
-
+import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 
 public class Layer {
-  private static final String TAG = Layer.class.getSimpleName();
 
   public enum LayerType {
     PreComp,
@@ -59,7 +54,7 @@
   private final int solidHeight;
   private final int solidColor;
   private final float timeStretch;
-  private final float startProgress;
+  private final float startFrame;
   private final int preCompWidth;
   private final int preCompHeight;
   @Nullable private final AnimatableTextFrame text;
@@ -71,7 +66,7 @@
   private Layer(List<ContentModel> shapes, LottieComposition composition, String layerName, long layerId,
       LayerType layerType, long parentId, @Nullable String refId, List<Mask> masks,
       AnimatableTransform transform, int solidWidth, int solidHeight, int solidColor,
-      float timeStretch, float startProgress, int preCompWidth, int preCompHeight,
+      float timeStretch, float startFrame, int preCompWidth, int preCompHeight,
       @Nullable AnimatableTextFrame text, @Nullable AnimatableTextProperties textProperties,
       List<Keyframe<Float>> inOutKeyframes, MatteType matteType,
       @Nullable AnimatableFloatValue timeRemapping) {
@@ -88,7 +83,7 @@
     this.solidHeight = solidHeight;
     this.solidColor = solidColor;
     this.timeStretch = timeStretch;
-    this.startProgress = startProgress;
+    this.startFrame = startFrame;
     this.preCompWidth = preCompWidth;
     this.preCompHeight = preCompHeight;
     this.text = text;
@@ -107,7 +102,7 @@
   }
 
   float getStartProgress() {
-    return startProgress;
+    return startFrame / composition.getDurationFrames();
   }
 
   List<Keyframe<Float>> getInOutKeyframes() {
@@ -229,103 +224,169 @@
           MatteType.None, null);
     }
 
-    public static Layer newInstance(JSONObject json, LottieComposition composition) {
-      String layerName = json.optString("nm");
-      String refId = json.optString("refId");
-
-      if (layerName.endsWith(".ai") || json.optString("cl", "").equals("ai")) {
-        composition.addWarning("Convert your Illustrator layers to shape layers.");
-      }
-
-      long layerId = json.optLong("ind");
+    public static Layer newInstance(
+        JsonReader reader, LottieComposition composition) throws IOException{
+      String layerName = null;
+      LayerType layerType = null;
+      String refId = null;
+      long layerId = 0;
       int solidWidth = 0;
       int solidHeight = 0;
       int solidColor = 0;
       int preCompWidth = 0;
       int preCompHeight = 0;
-      LayerType layerType;
-      int layerTypeInt = json.optInt("ty", -1);
-      if (layerTypeInt < LayerType.Unknown.ordinal()) {
-        layerType = LayerType.values()[layerTypeInt];
-      } else {
-        layerType = LayerType.Unknown;
-      }
+      long parentId = -1;
+      float timeStretch = 1f;
+      float startFrame = 0f;
+      float inFrame = 0f;
+      float outFrame = 0f;
+      String cl = null;
 
-      if (layerType == LayerType.Text && !Utils.isAtLeastVersion(composition, 4, 8, 0)) {
-        layerType = LayerType.Unknown;
-        composition.addWarning("Text is only supported on bodymovin >= 4.8.0");
-      }
-
-      long parentId = json.optLong("parent", -1);
-
-      if (layerType == LayerType.Solid) {
-        solidWidth = (int) (json.optInt("sw") * composition.getDpScale());
-        solidHeight = (int) (json.optInt("sh") * composition.getDpScale());
-        solidColor = Color.parseColor(json.optString("sc"));
-        if (L.DBG) {
-          Log.d(TAG, "\tSolid=" + Integer.toHexString(solidColor) + " " +
-              solidWidth + "x" + solidHeight + " " + composition.getBounds());
-        }
-      }
-
-      AnimatableTransform transform = AnimatableTransform.Factory.newInstance(json.optJSONObject("ks"),
-          composition);
-      MatteType matteType = MatteType.values()[json.optInt("tt")];
-      List<Mask> masks = new ArrayList<>();
-      JSONArray jsonMasks = json.optJSONArray("masksProperties");
-      if (jsonMasks != null) {
-        for (int i = 0; i < jsonMasks.length(); i++) {
-          Mask mask = Mask.Factory.newMask(jsonMasks.optJSONObject(i), composition);
-          masks.add(mask);
-        }
-      }
-
-      List<ContentModel> shapes = new ArrayList<>();
-      JSONArray shapesJson = json.optJSONArray("shapes");
-      if (shapesJson != null) {
-        for (int i = 0; i < shapesJson.length(); i++) {
-          ContentModel shape = ShapeGroup.shapeItemWithJson(shapesJson.optJSONObject(i), composition);
-          if (shape != null) {
-            shapes.add(shape);
-          }
-        }
-      }
-
+      MatteType matteType = MatteType.None;
+      AnimatableTransform transform = null;
       AnimatableTextFrame text = null;
       AnimatableTextProperties textProperties = null;
-      JSONObject textJson = json.optJSONObject("t");
-      if (textJson != null) {
-        text = AnimatableTextFrame.Factory.newInstance(textJson.optJSONObject("d"), composition);
-        JSONObject propertiesJson = textJson.optJSONArray("a").optJSONObject(0);
-        textProperties = AnimatableTextProperties.Factory.newInstance(propertiesJson, composition);
-      }
+      AnimatableFloatValue timeRemapping = null;
 
-      if (json.has("ef")) {
-        JSONArray effects = json.optJSONArray("ef");
-        String[] effectNames = new String[effects.length()];
-        for (int i = 0; i < effects.length(); i++) {
-          effectNames[i] = effects.optJSONObject(i).optString("nm");
+      List<Mask> masks = new ArrayList<>();
+      List<ContentModel> shapes = new ArrayList<>();
+
+
+      reader.beginObject();
+      while (reader.hasNext()) {
+        switch (reader.nextName()) {
+          case "nm":
+            layerName = reader.nextString();
+            break;
+          case "ind":
+            layerId = reader.nextInt();
+            break;
+          case "refId":
+            refId = reader.nextString();
+            break;
+          case "ty":
+            int layerTypeInt = reader.nextInt();
+            if (layerTypeInt < LayerType.Unknown.ordinal()) {
+              layerType = LayerType.values()[layerTypeInt];
+            } else {
+              layerType = LayerType.Unknown;
+            }
+            break;
+          case "parent":
+            parentId = reader.nextInt();
+            break;
+          case "sw":
+            solidWidth = (int) (reader.nextInt() * Utils.dpScale());
+            break;
+          case "sh":
+            solidHeight = (int) (reader.nextInt() * Utils.dpScale());
+            break;
+          case "sc":
+            solidColor = Color.parseColor(reader.nextString());
+            break;
+          case "ks":
+            transform = AnimatableTransform.Factory.newInstance(reader, composition);
+            break;
+          case "tt":
+            matteType = MatteType.values()[reader.nextInt()];
+            break;
+          case "masksProperties":
+            reader.beginArray();
+            while (reader.hasNext()) {
+              masks.add(Mask.Factory.newMask(reader, composition));
+            }
+            reader.endArray();
+            break;
+          case "shapes":
+            reader.beginArray();
+            while (reader.hasNext()) {
+              ContentModel shape = ShapeGroup.shapeItemWithJson(reader, composition);
+              if (shape != null) {
+                shapes.add(shape);
+              }
+            }
+            reader.endArray();
+            break;
+          case "t":
+            reader.beginObject();
+            while (reader.hasNext()) {
+              switch (reader.nextName()) {
+                case "d":
+                  text = AnimatableTextFrame.Factory.newInstance(reader, composition);
+                  break;
+                case "a":
+                  reader.beginArray();
+                  if (reader.hasNext()) {
+                    textProperties = AnimatableTextProperties.Factory.newInstance(reader, composition);
+                  }
+                  while (reader.hasNext()) {
+                    reader.skipValue();
+                  }
+                  reader.endArray();
+                  break;
+                default:
+                  reader.skipValue();
+              }
+            }
+            reader.endObject();
+            break;
+          case "ef":
+            reader.beginArray();
+            List<String> effectNames = new ArrayList<>();
+            while (reader.hasNext()) {
+              reader.beginObject();
+              while (reader.hasNext()) {
+                switch (reader.nextName()) {
+                  case "nm":
+                    effectNames.add(reader.nextString());
+                    break;
+                  default:
+                    reader.skipValue();
+
+                }
+              }
+              reader.endObject();
+            }
+            reader.endArray();
+            composition.addWarning("Lottie doesn't support layer effects. If you are using them for " +
+                " fills, strokes, trim paths etc. then try adding them directly as contents " +
+                " in your shape. Found: " + effectNames);
+            break;
+          case "sr":
+            timeStretch = (float) reader.nextDouble();
+            break;
+          case "st":
+            startFrame = (float) reader.nextDouble();
+            break;
+          case "w":
+            preCompWidth = (int) (reader.nextInt() * Utils.dpScale());
+            break;
+          case "h":
+            preCompHeight = (int) (reader.nextInt() * Utils.dpScale());
+            break;
+          case "ip":
+            inFrame = (float) reader.nextDouble();
+            break;
+          case "op":
+            outFrame = (float) reader.nextDouble();
+            break;
+          case "tm":
+            timeRemapping = AnimatableFloatValue.Factory.newInstance(reader, composition, false);
+            break;
+          case "cl":
+            cl = reader.nextString();
+            break;
+          default:
+            reader.skipValue();
         }
-        composition.addWarning("Lottie doesn't support layer effects. If you are using them for " +
-            " fills, strokes, trim paths etc. then try adding them directly as contents " +
-            " in your shape. Found: " + Arrays.toString(effectNames));
       }
-
-      float timeStretch = (float) json.optDouble("sr", 1.0);
-      float startFrame = (float) json.optDouble("st");
-      float frames = composition.getDurationFrames();
-      float startProgress = startFrame / frames;
-
-      if (layerType == LayerType.PreComp) {
-        preCompWidth = (int) (json.optInt("w") * composition.getDpScale());
-        preCompHeight = (int) (json.optInt("h") * composition.getDpScale());
-      }
+      reader.endObject();
 
       // Bodymovin pre-scales the in frame and out frame by the time stretch. However, that will
       // cause the stretch to be double counted since the in out animation gets treated the same
       // as all other animations and will have stretch applied to it again.
-      float inFrame = json.optLong("ip") / timeStretch;
-      float outFrame = json.optLong("op") / timeStretch;
+      inFrame /= timeStretch;
+      outFrame /= timeStretch;
 
       List<Keyframe<Float>> inOutKeyframes = new ArrayList<>();
       // Before the in frame
@@ -344,14 +405,17 @@
           composition, 0f, 0f, null, outFrame, Float.MAX_VALUE);
       inOutKeyframes.add(outKeyframe);
 
-      AnimatableFloatValue timeRemapping = null;
-      if (json.has("tm")) {
-        timeRemapping =
-            AnimatableFloatValue.Factory.newInstance(json.optJSONObject("tm"), composition, false);
+      if (layerName.endsWith(".ai") || "ai".equals(cl)) {
+        composition.addWarning("Convert your Illustrator layers to shape layers.");
+      }
+
+      if (layerType == LayerType.Text && !Utils.isAtLeastVersion(composition, 4, 8, 0)) {
+        layerType = LayerType.Unknown;
+        composition.addWarning("Text is only supported on bodymovin >= 4.8.0");
       }
 
       return new Layer(shapes, composition, layerName, layerId, layerType, parentId, refId,
-          masks, transform, solidWidth, solidHeight, solidColor, timeStretch, startProgress,
+          masks, transform, solidWidth, solidHeight, solidColor, timeStretch, startFrame,
           preCompWidth, preCompHeight, text, textProperties, inOutKeyframes, matteType,
           timeRemapping);
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java
index a655ab2..1d3c775 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java
@@ -116,7 +116,7 @@
       strokePaint.setStrokeWidth(strokeWidthAnimation.getValue());
     } else {
       float parentScale = Utils.getScale(parentMatrix);
-      strokePaint.setStrokeWidth(documentData.strokeWidth * composition.getDpScale() * parentScale);
+      strokePaint.setStrokeWidth(documentData.strokeWidth * Utils.dpScale() * parentScale);
     }
 
     if (lottieDrawable.useTextGlyphs()) {
@@ -144,7 +144,7 @@
         continue;
       }
       drawCharacterAsGlyph(character, parentMatrix, fontScale, documentData, canvas);
-      float tx = (float) character.getWidth() * fontScale * composition.getDpScale() * parentScale;
+      float tx = (float) character.getWidth() * fontScale * Utils.dpScale() * parentScale;
       // Add tracking
       float tracking = documentData.tracking / 10f;
       if (trackingAnimation != null) {
@@ -168,7 +168,7 @@
       text = textDelegate.getTextInternal(text);
     }
     fillPaint.setTypeface(typeface);
-    fillPaint.setTextSize(documentData.size * composition.getDpScale());
+    fillPaint.setTextSize((float) (documentData.size * Utils.dpScale()));
     strokePaint.setTypeface(fillPaint.getTypeface());
     strokePaint.setTextSize(fillPaint.getTextSize());
     for (int i = 0; i < text.length(); i++) {
@@ -197,7 +197,7 @@
       Path path = contentGroups.get(j).getPath();
       path.computeBounds(rectF, false);
       matrix.set(parentMatrix);
-      matrix.preTranslate(0, (float) -documentData.baselineShift * composition.getDpScale());
+      matrix.preTranslate(0, (float) -documentData.baselineShift * Utils.dpScale());
       matrix.preScale(fontScale, fontScale);
       path.transform(matrix);
       if (documentData.strokeOverFill) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/utils/JsonUtils.java b/lottie/src/main/java/com/airbnb/lottie/utils/JsonUtils.java
index 10bce9d..330a325 100644
--- a/lottie/src/main/java/com/airbnb/lottie/utils/JsonUtils.java
+++ b/lottie/src/main/java/com/airbnb/lottie/utils/JsonUtils.java
@@ -1,40 +1,113 @@
 package com.airbnb.lottie.utils;
 
+import android.graphics.Color;
 import android.graphics.PointF;
+import android.support.annotation.ColorInt;
+import android.util.JsonReader;
+import android.util.JsonToken;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 
 public class JsonUtils {
   private JsonUtils() {
   }
 
-  public static PointF pointFromJsonObject(JSONObject values, float scale) {
-    return new PointF(
-        valueFromObject(values.opt("x")) * scale,
-        valueFromObject(values.opt("y")) * scale);
-  }
-
-  public static PointF pointFromJsonArray(JSONArray values, float scale) {
-    if (values.length() < 2) {
-      throw new IllegalArgumentException("Unable to parse point for " + values);
+  /**
+   * [r,g,b]
+   */
+  @ColorInt public static int jsonToColor(JsonReader reader) throws IOException {
+    reader.beginArray();
+    int r = (int) (reader.nextDouble() * 255);
+    int g = (int) (reader.nextDouble() * 255);
+    int b = (int) (reader.nextDouble() * 255);
+    while (reader.hasNext()) {
+      reader.skipValue();
     }
-    return new PointF(
-        (float) values.optDouble(0, 1) * scale,
-        (float) values.optDouble(1, 1) * scale);
+    reader.endArray();
+    return Color.argb(255, r, g, b);
   }
 
-  public static float valueFromObject(Object object) {
-    if (object instanceof Float) {
-      return (float) object;
-    } else if (object instanceof Integer) {
-      return (Integer) object;
-    } else if (object instanceof Double) {
-      return (float) (double) object;
-    } else if (object instanceof JSONArray) {
-      return (float) ((JSONArray) object).optDouble(0);
-    } else {
-      return 0;
+  public static List<PointF> jsonToPoints(JsonReader reader, float scale) throws IOException {
+    List<PointF> points = new ArrayList<>();
+
+    reader.beginArray();
+    while (reader.peek() == JsonToken.BEGIN_ARRAY) {
+      reader.beginArray();
+      points.add(jsonToPoint(reader, scale));
+      reader.endArray();
+    }
+    reader.endArray();
+    return points;
+  }
+
+  public static PointF jsonToPoint(JsonReader reader, float scale) throws IOException {
+    switch (reader.peek()) {
+      case NUMBER: return jsonNumbersToPoint(reader, scale);
+      case BEGIN_ARRAY: return jsonArrayToPoint(reader, scale);
+      case BEGIN_OBJECT: return jsonObjectToPoint(reader, scale);
+      default: throw new IllegalArgumentException("Unknown point starts with " + reader.peek());
+    }
+  }
+
+  private static PointF jsonNumbersToPoint(JsonReader reader, float scale) throws IOException {
+    float x = (float) reader.nextDouble();
+    float y = (float) reader.nextDouble();
+    while (reader.hasNext()) {
+      reader.skipValue();
+    }
+    return new PointF(x * scale, y * scale);
+  }
+
+  private static PointF jsonArrayToPoint(JsonReader reader, float scale) throws IOException {
+    float x;
+    float y;
+    reader.beginArray();
+    x = (float) reader.nextDouble();
+    y = (float) reader.nextDouble();
+    while (reader.peek() != JsonToken.END_ARRAY) {
+      reader.skipValue();
+    }
+    reader.endArray();
+    return new PointF(x * scale, y * scale);
+  }
+
+  private static PointF jsonObjectToPoint(JsonReader reader, float scale) throws IOException {
+    float x = 0f;
+    float y = 0f;
+    reader.beginObject();
+    while (reader.hasNext()) {
+      switch (reader.nextName()) {
+        case "x":
+          x = valueFromObject(reader);
+          break;
+        case "y":
+          y = valueFromObject(reader);
+          break;
+        default:
+          reader.skipValue();
+      }
+    }
+    reader.endObject();
+    return new PointF(x * scale, y * scale);
+  }
+
+  public static float valueFromObject(JsonReader reader) throws IOException {
+    JsonToken token = reader.peek();
+    switch (token) {
+      case NUMBER:
+        return (float) reader.nextDouble();
+      case BEGIN_ARRAY:
+        reader.beginArray();
+        float val = (float) reader.nextDouble();
+        while (reader.hasNext()) {
+          reader.skipValue();
+        }
+        reader.endArray();
+        return val;
+      default:
+        throw new IllegalArgumentException("Unknown value for token of type " + token);
     }
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/utils/Utils.java b/lottie/src/main/java/com/airbnb/lottie/utils/Utils.java
index fa2a46c..fb3ee34 100644
--- a/lottie/src/main/java/com/airbnb/lottie/utils/Utils.java
+++ b/lottie/src/main/java/com/airbnb/lottie/utils/Utils.java
@@ -1,6 +1,7 @@
 package com.airbnb.lottie.utils;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Matrix;
 import android.graphics.Path;
 import android.graphics.PathMeasure;
@@ -24,6 +25,7 @@
   private static DisplayMetrics displayMetrics;
   private static final float[] points = new float[4];
   private static final float SQRT_2 = (float) Math.sqrt(2);
+  private static float dpScale = -1;
 
   private Utils() {}
 
@@ -213,4 +215,11 @@
           Settings.System.ANIMATOR_DURATION_SCALE, 1.0f);
     }
   }
+
+  public static float dpScale() {
+    if (dpScale == -1) {
+      dpScale = Resources.getSystem().getDisplayMetrics().density;
+    }
+    return dpScale;
+  }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/value/ScaleXY.java b/lottie/src/main/java/com/airbnb/lottie/value/ScaleXY.java
index 9f69eb4..069545f 100644
--- a/lottie/src/main/java/com/airbnb/lottie/value/ScaleXY.java
+++ b/lottie/src/main/java/com/airbnb/lottie/value/ScaleXY.java
@@ -1,8 +1,11 @@
 package com.airbnb.lottie.value;
 
+import android.util.JsonReader;
+import android.util.JsonToken;
+
 import com.airbnb.lottie.model.animatable.AnimatableValue;
 
-import org.json.JSONArray;
+import java.io.IOException;
 
 public class ScaleXY {
   private final float scaleX;
@@ -35,11 +38,20 @@
     private Factory() {
     }
 
-    @Override public ScaleXY valueFromObject(Object object, float scale) {
-      JSONArray array = (JSONArray) object;
-      return new ScaleXY(
-          (float) array.optDouble(0, 1) / 100f * scale,
-          (float) array.optDouble(1, 1) / 100f * scale);
+    @Override public ScaleXY valueFromObject(JsonReader reader, float scale) throws IOException {
+      boolean isArray = reader.peek() == JsonToken.BEGIN_ARRAY;
+      if (isArray) {
+        reader.beginArray();
+      }
+      float sx = (float) reader.nextDouble();
+      float sy = (float) reader.nextDouble();
+      while (reader.hasNext()) {
+        reader.skipValue();
+      }
+      if (isArray) {
+        reader.endArray();
+      }
+      return new ScaleXY(sx / 100f * scale, sy / 100f * scale);
     }
   }
 }
diff --git a/lottie/src/test/java/com/airbnb/lottie/KeyPathTest.java b/lottie/src/test/java/com/airbnb/lottie/KeyPathTest.java
index 422e197..d474fdc 100644
--- a/lottie/src/test/java/com/airbnb/lottie/KeyPathTest.java
+++ b/lottie/src/test/java/com/airbnb/lottie/KeyPathTest.java
@@ -1,17 +1,16 @@
 package com.airbnb.lottie;
 
-import android.app.Application;
+import android.util.JsonReader;
 
 import com.airbnb.lottie.model.KeyPath;
 
-import org.json.JSONException;
-import org.json.JSONObject;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
 
+import java.io.IOException;
+import java.io.StringReader;
 import java.util.List;
 
 import static junit.framework.Assert.assertEquals;
@@ -32,13 +31,12 @@
 
   @Before
   public void setupDrawable() {
-    Application context = RuntimeEnvironment.application;
     lottieDrawable = new LottieDrawable();
     try {
       LottieComposition composition = LottieComposition.Factory
-          .fromJsonSync(context.getResources(), new JSONObject(Fixtures.SQUARES));
+          .fromJsonSync(new JsonReader(new StringReader(Fixtures.SQUARES)));
       lottieDrawable.setComposition(composition);
-    } catch (JSONException e) {
+    } catch (IOException e) {
       throw new IllegalStateException(e);
     }
   }