Use moshi implementation for json parsing (#1234)

There's a dependency on OKIO & 8 classes copied from Moshi. Need to figure out best path forward. Ideally we don't depend on all of Moshi but still get the gains of the new JsonReader & Options api. This should fix the random Android 8 crashes.

Performance
I ran the snapshot tests with ~1800 animations and summed up just the parsing time. The old parsing code took 13,145ms and 13,645ms on each test run (avg 13,395ms). The new code took 12,858ms each time. There aren't enough trials to deduce statistical differences but if these numbers hold, the new code parses ~5% faster.
It does reduce memory allocations during parsing which may contribute to the performance improvement.

I'm leaning on merging this to hopefully fix #667
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
index aa1f270..e5bf823 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
@@ -24,6 +24,7 @@
 import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtility
 import com.amazonaws.services.s3.AmazonS3Client
 import com.amazonaws.services.s3.model.S3ObjectSummary
+import junit.framework.Assert.assertNull
 import kotlinx.coroutines.*
 import kotlinx.coroutines.channels.ReceiveChannel
 import kotlinx.coroutines.channels.produce
diff --git a/lottie/build.gradle b/lottie/build.gradle
index 7b45e1b..e036a2d 100644
--- a/lottie/build.gradle
+++ b/lottie/build.gradle
@@ -30,6 +30,10 @@
   testImplementation "org.mockito:mockito-core:2.15.0"
   testImplementation 'junit:junit:4.12'
   testImplementation "org.robolectric:robolectric:4.0-alpha-3"
+  // Do not ugprade to 2.0 because it will bring in Kotlin as a transitive dependency.
+  implementation("com.squareup.okio:okio:1.17.4")
+
+
 }
 
 task javadoc(type: Javadoc) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 459227f..1263b44 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -17,18 +17,21 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RawRes;
 import androidx.appcompat.widget.AppCompatImageView;
+import okio.Okio;
+
 import android.text.TextUtils;
 import android.util.AttributeSet;
-import android.util.JsonReader;
 import android.util.Log;
 import android.view.View;
 
 import com.airbnb.lottie.model.KeyPath;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.utils.Utils;
 import com.airbnb.lottie.value.LottieFrameInfo;
 import com.airbnb.lottie.value.LottieValueCallback;
 import com.airbnb.lottie.value.SimpleLottieValueCallback;
 
+import java.io.ByteArrayInputStream;
 import java.io.StringReader;
 import java.util.HashSet;
 import java.util.List;
@@ -327,7 +330,7 @@
    * JSONObject never has to be done.
    */
   public void setAnimationFromJson(String jsonString, @Nullable String cacheKey) {
-    setAnimation(new JsonReader(new StringReader(jsonString)), cacheKey);
+    setAnimation(JsonReader.of(Okio.buffer(Okio.source(new ByteArrayInputStream(jsonString.getBytes())))), cacheKey);
   }
 
   /**
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
index 4ffe434..b974506 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
@@ -9,12 +9,13 @@
 import androidx.annotation.WorkerThread;
 import androidx.collection.LongSparseArray;
 import androidx.collection.SparseArrayCompat;
-import android.util.JsonReader;
+import android.util.Log;
 
 import com.airbnb.lottie.model.Font;
 import com.airbnb.lottie.model.FontCharacter;
 import com.airbnb.lottie.model.Marker;
 import com.airbnb.lottie.model.layer.Layer;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.utils.Logger;
 
 import org.json.JSONObject;
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java b/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java
index 15399a0..97971ad 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java
@@ -4,34 +4,36 @@
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
-import androidx.annotation.Nullable;
-import androidx.annotation.RawRes;
-import androidx.annotation.WorkerThread;
-import android.util.JsonReader;
-import android.util.Log;
 
 import com.airbnb.lottie.model.LottieCompositionCache;
 import com.airbnb.lottie.network.NetworkFetcher;
-import com.airbnb.lottie.parser.LottieCompositionParser;
+import com.airbnb.lottie.parser.LottieCompositionMoshiParser;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import com.airbnb.lottie.utils.Utils;
 import org.json.JSONObject;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.StringReader;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.Callable;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
+import androidx.annotation.Nullable;
+import androidx.annotation.RawRes;
+import androidx.annotation.WorkerThread;
+
+import static com.airbnb.lottie.parser.moshi.JsonReader.*;
 import static com.airbnb.lottie.utils.Utils.closeQuietly;
+import static okio.Okio.buffer;
+import static okio.Okio.source;
 
 /**
  * Helpers to create or cache a LottieComposition.
- *
+ * <p>
  * All factory methods take a cache key. The animation will be stored in an LRU cache for future use.
  * In-progress tasks will also be held so they can be returned for subsequent requests for the same
  * animation prior to the cache being populated.
@@ -64,7 +66,8 @@
   public static LottieTask<LottieComposition> fromUrl(final Context context, final String url) {
     String urlCacheKey = "url_" + url;
     return cache(urlCacheKey, new Callable<LottieResult<LottieComposition>>() {
-      @Override public LottieResult<LottieComposition> call() {
+      @Override
+      public LottieResult<LottieComposition> call() {
         return NetworkFetcher.fetchSync(context, url);
       }
     });
@@ -91,7 +94,8 @@
     // Prevent accidentally leaking an Activity.
     final Context appContext = context.getApplicationContext();
     return cache(fileName, new Callable<LottieResult<LottieComposition>>() {
-      @Override public LottieResult<LottieComposition> call() {
+      @Override
+      public LottieResult<LottieComposition> call() {
         return fromAssetSync(appContext, fileName);
       }
     });
@@ -117,6 +121,7 @@
     }
   }
 
+
   /**
    * Parse an animation from raw/res. This is recommended over putting your animation in assets because
    * it uses a hard reference to R.
@@ -126,7 +131,8 @@
     // Prevent accidentally leaking an Activity.
     final Context appContext = context.getApplicationContext();
     return cache(rawResCacheKey(rawRes), new Callable<LottieResult<LottieComposition>>() {
-      @Override public LottieResult<LottieComposition> call() {
+      @Override
+      public LottieResult<LottieComposition> call() {
         return fromRawResSync(appContext, rawRes);
       }
     });
@@ -157,7 +163,8 @@
    */
   public static LottieTask<LottieComposition> fromJsonInputStream(final InputStream stream, @Nullable final String cacheKey) {
     return cache(cacheKey, new Callable<LottieResult<LottieComposition>>() {
-      @Override public LottieResult<LottieComposition> call() {
+      @Override
+      public LottieResult<LottieComposition> call() {
         return fromJsonInputStreamSync(stream, cacheKey);
       }
     });
@@ -171,10 +178,11 @@
     return fromJsonInputStreamSync(stream, cacheKey, true);
   }
 
+
   @WorkerThread
   private static LottieResult<LottieComposition> fromJsonInputStreamSync(InputStream stream, @Nullable String cacheKey, boolean close) {
     try {
-      return fromJsonReaderSync(new JsonReader(new InputStreamReader(stream)), cacheKey);
+      return fromJsonReaderSync(of(buffer(source(stream))), cacheKey);
     } finally {
       if (close) {
         closeQuietly(stream);
@@ -182,13 +190,15 @@
     }
   }
 
+
   /**
    * @see #fromJsonSync(JSONObject, String)
    */
   @Deprecated
   public static LottieTask<LottieComposition> fromJson(final JSONObject json, @Nullable final String cacheKey) {
     return cache(cacheKey, new Callable<LottieResult<LottieComposition>>() {
-      @Override public LottieResult<LottieComposition> call() {
+      @Override
+      public LottieResult<LottieComposition> call() {
         //noinspection deprecation
         return fromJsonSync(json, cacheKey);
       }
@@ -211,7 +221,8 @@
    */
   public static LottieTask<LottieComposition> fromJsonString(final String json, @Nullable final String cacheKey) {
     return cache(cacheKey, new Callable<LottieResult<LottieComposition>>() {
-      @Override public LottieResult<LottieComposition> call() {
+      @Override
+      public LottieResult<LottieComposition> call() {
         return fromJsonStringSync(json, cacheKey);
       }
     });
@@ -223,29 +234,32 @@
    */
   @WorkerThread
   public static LottieResult<LottieComposition> fromJsonStringSync(String json, @Nullable String cacheKey) {
-    return fromJsonReaderSync(new JsonReader(new StringReader(json)), cacheKey);
+
+
+    ByteArrayInputStream stream = new ByteArrayInputStream(json.getBytes());
+    return fromJsonReaderSync(of(buffer(source(stream))), cacheKey);
   }
 
   public static LottieTask<LottieComposition> fromJsonReader(final JsonReader reader, @Nullable final String cacheKey) {
     return cache(cacheKey, new Callable<LottieResult<LottieComposition>>() {
-      @Override public LottieResult<LottieComposition> call() {
+      @Override
+      public LottieResult<LottieComposition> call() {
         return fromJsonReaderSync(reader, cacheKey);
       }
     });
   }
 
-  /**
-   * Return a LottieComposition for the specified json.
-   */
+
   @WorkerThread
-  public static LottieResult<LottieComposition> fromJsonReaderSync(JsonReader reader, @Nullable String cacheKey) {
+  public static LottieResult<LottieComposition> fromJsonReaderSync(com.airbnb.lottie.parser.moshi.JsonReader reader, @Nullable String cacheKey) {
     return fromJsonReaderSyncInternal(reader, cacheKey, true);
   }
 
+
   private static LottieResult<LottieComposition> fromJsonReaderSyncInternal(
-          JsonReader reader, @Nullable String cacheKey, boolean close) {
+      com.airbnb.lottie.parser.moshi.JsonReader reader, @Nullable String cacheKey, boolean close) {
     try {
-      LottieComposition composition = LottieCompositionParser.parse(reader);
+      LottieComposition composition = LottieCompositionMoshiParser.parse(reader);
       LottieCompositionCache.getInstance().put(cacheKey, composition);
       return new LottieResult<>(composition);
     } catch (Exception e) {
@@ -257,9 +271,11 @@
     }
   }
 
+
   public static LottieTask<LottieComposition> fromZipStream(final ZipInputStream inputStream, @Nullable final String cacheKey) {
     return cache(cacheKey, new Callable<LottieResult<LottieComposition>>() {
-      @Override public LottieResult<LottieComposition> call() {
+      @Override
+      public LottieResult<LottieComposition> call() {
         return fromZipStreamSync(inputStream, cacheKey);
       }
     });
@@ -290,8 +306,8 @@
         final String entryName = entry.getName();
         if (entryName.contains("__MACOSX")) {
           inputStream.closeEntry();
-        } else if (entryName.contains(".json")) {
-          JsonReader reader = new JsonReader(new InputStreamReader(inputStream));
+        } else if (entry.getName().contains(".json")) {
+          com.airbnb.lottie.parser.moshi.JsonReader reader = of(buffer(source(inputStream)));
           composition = LottieCompositionFactory.fromJsonReaderSyncInternal(reader, null, false).getValue();
         } else if (entryName.contains(".png") || entryName.contains(".webp")) {
           String[] splitName = entryName.split("/");
@@ -346,7 +362,7 @@
    * Then, add the new task to the task cache and set up listeners so it gets cleared when done.
    */
   private static LottieTask<LottieComposition> cache(
-          @Nullable final String cacheKey, Callable<LottieResult<LottieComposition>> callable) {
+      @Nullable final String cacheKey, Callable<LottieResult<LottieComposition>> callable) {
     final LottieComposition cachedComposition = cacheKey == null ? null : LottieCompositionCache.getInstance().get(cacheKey);
     if (cachedComposition != null) {
       return new LottieTask<>(new Callable<LottieResult<LottieComposition>>() {
@@ -362,7 +378,8 @@
 
     LottieTask<LottieComposition> task = new LottieTask<>(callable);
     task.addListener(new LottieListener<LottieComposition>() {
-      @Override public void onResult(LottieComposition result) {
+      @Override
+      public void onResult(LottieComposition result) {
         if (cacheKey != null) {
           LottieCompositionCache.getInstance().put(cacheKey, result);
         }
@@ -370,7 +387,8 @@
       }
     });
     task.addFailureListener(new LottieListener<Throwable>() {
-      @Override public void onResult(Throwable result) {
+      @Override
+      public void onResult(Throwable result) {
         taskCache.remove(cacheKey);
       }
     });
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/content/RectangleContent.java b/lottie/src/main/java/com/airbnb/lottie/animation/content/RectangleContent.java
index c5606e4..919695a 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/content/RectangleContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/content/RectangleContent.java
@@ -3,7 +3,6 @@
 import android.graphics.Path;
 import android.graphics.PointF;
 import android.graphics.RectF;
-import androidx.annotation.Nullable;
 
 import com.airbnb.lottie.LottieDrawable;
 import com.airbnb.lottie.LottieProperty;
@@ -18,6 +17,8 @@
 
 import java.util.List;
 
+import androidx.annotation.Nullable;
+
 public class RectangleContent
     implements BaseKeyframeAnimation.AnimationListener, KeyPathElementContent, PathContent {
   private final Path path = new Path();
@@ -50,11 +51,13 @@
     cornerRadiusAnimation.addUpdateListener(this);
   }
 
-  @Override public String getName() {
+  @Override
+  public String getName() {
     return name;
   }
 
-  @Override public void onValueChanged() {
+  @Override
+  public void onValueChanged() {
     invalidate();
   }
 
@@ -63,7 +66,8 @@
     lottieDrawable.invalidateSelf();
   }
 
-  @Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
+  @Override
+  public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
     for (int i = 0; i < contentsBefore.size(); i++) {
       Content content = contentsBefore.get(i);
       if (content instanceof TrimPathContent &&
@@ -75,7 +79,8 @@
     }
   }
 
-  @Override public Path getPath() {
+  @Override
+  public Path getPath() {
     if (isPathValid) {
       return path;
     }
@@ -91,7 +96,7 @@
     float halfWidth = size.x / 2f;
     float halfHeight = size.y / 2f;
     float radius = cornerRadiusAnimation == null ?
-            0f : ((FloatKeyframeAnimation) cornerRadiusAnimation).getFloatValue();
+        0f : ((FloatKeyframeAnimation) cornerRadiusAnimation).getFloatValue();
     float maxRadius = Math.min(halfWidth, halfHeight);
     if (radius > maxRadius) {
       radius = maxRadius;
@@ -149,18 +154,20 @@
     return path;
   }
 
-  @Override public void resolveKeyPath(KeyPath keyPath, int depth, List<KeyPath> accumulator,
-      KeyPath currentPartialKeyPath) {
+  @Override
+  public void resolveKeyPath(KeyPath keyPath, int depth, List<KeyPath> accumulator,
+                             KeyPath currentPartialKeyPath) {
     MiscUtils.resolveKeyPath(keyPath, depth, accumulator, currentPartialKeyPath, this);
   }
 
-  @Override public <T> void addValueCallback(T property, @Nullable LottieValueCallback<T> callback) {
+  @Override
+  public <T> void addValueCallback(T property, @Nullable LottieValueCallback<T> callback) {
     if (property == LottieProperty.RECTANGLE_SIZE) {
-        sizeAnimation.setValueCallback((LottieValueCallback<PointF>) callback);
+      sizeAnimation.setValueCallback((LottieValueCallback<PointF>) callback);
     } else if (property == LottieProperty.POSITION) {
-        positionAnimation.setValueCallback((LottieValueCallback<PointF>) callback);
+      positionAnimation.setValueCallback((LottieValueCallback<PointF>) callback);
     } else if (property == LottieProperty.CORNER_RADIUS) {
-        cornerRadiusAnimation.setValueCallback((LottieValueCallback<Float>) callback);
+      cornerRadiusAnimation.setValueCallback((LottieValueCallback<Float>) callback);
     }
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/AnimatablePathValueParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/AnimatablePathValueParser.java
index 8ce00b1..c9bb639 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/AnimatablePathValueParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/AnimatablePathValueParser.java
@@ -1,10 +1,10 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.PointF;
-import android.util.JsonReader;
 import android.util.JsonToken;
 
 import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.value.Keyframe;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
 import com.airbnb.lottie.model.animatable.AnimatablePathValue;
@@ -18,12 +18,18 @@
 
 public class AnimatablePathValueParser {
 
+  private static JsonReader.Options NAMES = JsonReader.Options.of(
+      "k",
+      "x",
+      "y"
+  );
+
   private AnimatablePathValueParser() {}
 
   public static AnimatablePathValue parse(
       JsonReader reader, LottieComposition composition) throws IOException {
     List<Keyframe<PointF>> keyframes = new ArrayList<>();
-    if (reader.peek() == JsonToken.BEGIN_ARRAY) {
+    if (reader.peek() == JsonReader.Token.BEGIN_ARRAY) {
       reader.beginArray();
       while (reader.hasNext()) {
         keyframes.add(PathKeyframeParser.parse(reader, composition));
@@ -49,21 +55,21 @@
     boolean hasExpressions = false;
 
     reader.beginObject();
-    while (reader.peek() != JsonToken.END_OBJECT) {
-      switch (reader.nextName()) {
-        case "k":
+    while (reader.peek() != JsonReader.Token.END_OBJECT) {
+      switch (reader.selectName(NAMES)) {
+        case 0:
           pathAnimation = AnimatablePathValueParser.parse(reader, composition);
           break;
-        case "x":
-          if (reader.peek() == JsonToken.STRING) {
+        case 1:
+          if (reader.peek() == JsonReader.Token.STRING) {
             hasExpressions = true;
             reader.skipValue();
           } else {
             xAnimation = AnimatableValueParser.parseFloat(reader, composition);
           }
           break;
-        case "y":
-          if (reader.peek() == JsonToken.STRING) {
+        case 2:
+          if (reader.peek() == JsonReader.Token.STRING) {
             hasExpressions = true;
             reader.skipValue();
           } else {
@@ -71,6 +77,7 @@
           }
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTextPropertiesParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTextPropertiesParser.java
index 2c0089c..1d3d0b9 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTextPropertiesParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTextPropertiesParser.java
@@ -1,16 +1,23 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
-
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatableColorValue;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
 import com.airbnb.lottie.model.animatable.AnimatableTextProperties;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
 public class AnimatableTextPropertiesParser {
 
+  private static JsonReader.Options PROPERTIES_NAMES = JsonReader.Options.of("a");
+  private static JsonReader.Options ANIMATABLE_PROPERTIES_NAMES = JsonReader.Options.of(
+      "fc",
+      "sc",
+      "sw",
+      "t"
+  );
+
   private AnimatableTextPropertiesParser() {}
 
   public static AnimatableTextProperties parse(
@@ -19,11 +26,12 @@
 
     reader.beginObject();
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "a":
+      switch (reader.selectName(PROPERTIES_NAMES)) {
+        case 0:
           anim = parseAnimatableTextProperties(reader, composition);
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
@@ -44,20 +52,21 @@
 
     reader.beginObject();
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "fc":
+      switch (reader.selectName(ANIMATABLE_PROPERTIES_NAMES)) {
+        case 0:
           color = AnimatableValueParser.parseColor(reader, composition);
           break;
-        case "sc":
+        case 1:
           stroke = AnimatableValueParser.parseColor(reader, composition);
           break;
-        case "sw":
+        case 2:
           strokeWidth = AnimatableValueParser.parseFloat(reader, composition);
           break;
-        case "t":
+        case 3:
           tracking = AnimatableValueParser.parseFloat(reader, composition);
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTransformParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTransformParser.java
index 7930181..30eefdc 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTransformParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTransformParser.java
@@ -1,7 +1,6 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.PointF;
-import android.util.JsonReader;
 import android.util.JsonToken;
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
@@ -11,6 +10,7 @@
 import com.airbnb.lottie.model.animatable.AnimatableSplitDimensionPathValue;
 import com.airbnb.lottie.model.animatable.AnimatableTransform;
 import com.airbnb.lottie.model.animatable.AnimatableValue;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.value.Keyframe;
 
 import java.io.IOException;
@@ -20,6 +20,21 @@
   private AnimatableTransformParser() {
   }
 
+
+  private static JsonReader.Options NAMES = JsonReader.Options.of(
+      "a",
+      "p",
+      "s",
+      "rz",
+      "r",
+      "o",
+      "so",
+      "eo",
+      "sk",
+      "sa"
+  );
+  private static JsonReader.Options ANIMATABLE_NAMES = JsonReader.Options.of("k");
+
   public static AnimatableTransform parse(
       JsonReader reader, LottieComposition composition) throws IOException {
     AnimatablePathValue anchorPoint = null;
@@ -32,33 +47,36 @@
     AnimatableFloatValue skew = null;
     AnimatableFloatValue skewAngle = null;
 
-    boolean isObject = reader.peek() == JsonToken.BEGIN_OBJECT;
+    boolean isObject = reader.peek() == JsonReader.Token.BEGIN_OBJECT;
     if (isObject) {
       reader.beginObject();
     }
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "a":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           reader.beginObject();
           while (reader.hasNext()) {
-            if (reader.nextName().equals("k")) {
-              anchorPoint = AnimatablePathValueParser.parse(reader, composition);
-            } else {
-              reader.skipValue();
+            switch (reader.selectName(ANIMATABLE_NAMES)) {
+              case 0:
+                anchorPoint = AnimatablePathValueParser.parse(reader, composition);
+                break;
+                default:
+                  reader.skipName();
+                  reader.skipValue();
             }
           }
           reader.endObject();
           break;
-        case "p":
+        case 1:
           position =
               AnimatablePathValueParser.parseSplitPath(reader, composition);
           break;
-        case "s":
+        case 2:
           scale = AnimatableValueParser.parseScale(reader, composition);
           break;
-        case "rz":
+        case 3:
           composition.addWarning("Lottie doesn't support 3D layers.");
-        case "r":
+        case 4:
           /**
            * Sometimes split path rotation gets exported like:
            *         "rz": {
@@ -76,22 +94,23 @@
             rotation.getKeyframes().set(0, new Keyframe(composition, 0f, 0f, null, 0f, composition.getEndFrame()));
           }
           break;
-        case "o":
+        case 5:
           opacity = AnimatableValueParser.parseInteger(reader, composition);
           break;
-        case "so":
+        case 6:
           startOpacity = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
-        case "eo":
+        case 7:
           endOpacity = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
-        case "sk":
+        case 8:
           skew = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
-        case "sa":
+        case 9:
           skewAngle = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableValueParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableValueParser.java
index b836aad..631ae22 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableValueParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableValueParser.java
@@ -1,9 +1,9 @@
 package com.airbnb.lottie.parser;
 
 import androidx.annotation.Nullable;
-import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.value.Keyframe;
 import com.airbnb.lottie.model.animatable.AnimatableColorValue;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/CircleShapeParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/CircleShapeParser.java
index 4f31633..5d55aa3 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/CircleShapeParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/CircleShapeParser.java
@@ -1,17 +1,25 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.PointF;
-import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatablePointValue;
 import com.airbnb.lottie.model.animatable.AnimatableValue;
 import com.airbnb.lottie.model.content.CircleShape;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
 class CircleShapeParser {
 
+  private static JsonReader.Options NAMES = JsonReader.Options.of(
+      "nm",
+      "p",
+      "s",
+      "hd",
+      "d"
+  );
+
   private CircleShapeParser() {}
 
   static CircleShape parse(
@@ -23,24 +31,25 @@
     boolean hidden = false;
 
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "nm":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           name = reader.nextString();
           break;
-        case "p":
+        case 1:
           position = AnimatablePathValueParser.parseSplitPath(reader, composition);
           break;
-        case "s":
+        case 2:
           size = AnimatableValueParser.parsePoint(reader, composition);
           break;
-        case "hd":
+        case 3:
           hidden = reader.nextBoolean();
           break;
-        case "d":
+        case 4:
           // "d" is 2 for normal and 3 for reversed.
           reversed = reader.nextInt() == 3;
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ColorParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ColorParser.java
index c3193e3..6f61119 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/ColorParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ColorParser.java
@@ -1,9 +1,10 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.Color;
-import android.util.JsonReader;
 import android.util.JsonToken;
 
+import com.airbnb.lottie.parser.moshi.JsonReader;
+
 import java.io.IOException;
 
 public class ColorParser implements ValueParser<Integer> {
@@ -12,7 +13,7 @@
   private ColorParser() {}
 
   @Override public Integer parse(JsonReader reader, float scale) throws IOException {
-    boolean isArray = reader.peek() == JsonToken.BEGIN_ARRAY;
+    boolean isArray = reader.peek() == JsonReader.Token.BEGIN_ARRAY;
     if (isArray) {
       reader.beginArray();
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ContentModelParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ContentModelParser.java
index f2430d4..63d5f17 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/ContentModelParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ContentModelParser.java
@@ -1,17 +1,22 @@
 package com.airbnb.lottie.parser;
 
 import androidx.annotation.Nullable;
-import android.util.JsonReader;
-
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.content.ContentModel;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.utils.Logger;
 
 import java.io.IOException;
 
 class ContentModelParser {
 
-  private ContentModelParser() {}
+  private static JsonReader.Options NAMES = JsonReader.Options.of(
+      "ty",
+      "d"
+  );
+
+  private ContentModelParser() {
+  }
 
   @Nullable
   static ContentModel parse(JsonReader reader, LottieComposition composition)
@@ -25,14 +30,15 @@
     int d = 2;
     typeLoop:
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "ty":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           type = reader.nextString();
           break typeLoop;
-        case "d":
+        case 1:
           d = reader.nextInt();
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/DocumentDataParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/DocumentDataParser.java
index 3b057e4..f1ce6d8 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/DocumentDataParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/DocumentDataParser.java
@@ -1,18 +1,33 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
 
 import com.airbnb.lottie.model.DocumentData;
-
 import com.airbnb.lottie.model.DocumentData.Justification;
+import com.airbnb.lottie.parser.moshi.JsonReader;
+
 import java.io.IOException;
 
 public class DocumentDataParser implements ValueParser<DocumentData> {
   public static final DocumentDataParser INSTANCE = new DocumentDataParser();
+  private static final JsonReader.Options NAMES = JsonReader.Options.of(
+      "t",
+      "f",
+      "s",
+      "j",
+      "tr",
+      "lh",
+      "ls",
+      "fc",
+      "sc",
+      "sw",
+      "of"
+  );
 
-  private DocumentDataParser() {}
+  private DocumentDataParser() {
+  }
 
-  @Override public DocumentData parse(JsonReader reader, float scale) throws IOException {
+  @Override
+  public DocumentData parse(JsonReader reader, float scale) throws IOException {
     String text = null;
     String fontName = null;
     double size = 0;
@@ -27,17 +42,17 @@
 
     reader.beginObject();
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "t":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           text = reader.nextString();
           break;
-        case "f":
+        case 1:
           fontName = reader.nextString();
           break;
-        case "s":
+        case 2:
           size = reader.nextDouble();
           break;
-        case "j":
+        case 3:
           int justificationInt = reader.nextInt();
           if (justificationInt > Justification.CENTER.ordinal() || justificationInt < 0) {
             justification = Justification.CENTER;
@@ -45,28 +60,29 @@
             justification = Justification.values()[justificationInt];
           }
           break;
-        case "tr":
+        case 4:
           tracking = reader.nextInt();
           break;
-        case "lh":
+        case 5:
           lineHeight = reader.nextDouble();
           break;
-        case "ls":
+        case 6:
           baselineShift = reader.nextDouble();
           break;
-        case "fc":
+        case 7:
           fillColor = JsonUtils.jsonToColor(reader);
           break;
-        case "sc":
+        case 8:
           strokeColor = JsonUtils.jsonToColor(reader);
           break;
-        case "sw":
+        case 9:
           strokeWidth = reader.nextDouble();
           break;
-        case "of":
+        case 10:
           strokeOverFill = reader.nextBoolean();
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/FloatParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/FloatParser.java
index 1b03bc3..7995201 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/FloatParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/FloatParser.java
@@ -1,6 +1,7 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
+
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/FontCharacterParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/FontCharacterParser.java
index 2443ac9..a3c03d7 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/FontCharacterParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/FontCharacterParser.java
@@ -1,18 +1,27 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
-
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.FontCharacter;
 import com.airbnb.lottie.model.content.ShapeGroup;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
 class FontCharacterParser {
+  private static final JsonReader.Options NAMES = JsonReader.Options.of(
+      "ch",
+      "size",
+      "w",
+      "style",
+      "fFamily",
+      "data"
+  );
+  private static final JsonReader.Options DATA_NAMES = JsonReader.Options.of("shapes");
 
-  private FontCharacterParser() {}
+  private FontCharacterParser() {
+  }
 
   static FontCharacter parse(
       JsonReader reader, LottieComposition composition) throws IOException {
@@ -25,38 +34,42 @@
 
     reader.beginObject();
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "ch":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           character = reader.nextString().charAt(0);
           break;
-        case "size":
+        case 1:
           size = reader.nextDouble();
           break;
-        case "w":
+        case 2:
           width = reader.nextDouble();
           break;
-        case "style":
+        case 3:
           style = reader.nextString();
           break;
-        case "fFamily":
+        case 4:
           fontFamily = reader.nextString();
           break;
-        case "data":
+        case 5:
           reader.beginObject();
           while (reader.hasNext()) {
-            if ("shapes".equals(reader.nextName())) {
-              reader.beginArray();
-              while (reader.hasNext()) {
-                shapes.add((ShapeGroup) ContentModelParser.parse(reader, composition));
-              }
-              reader.endArray();
-            } else {
-              reader.skipValue();
+            switch (reader.selectName(DATA_NAMES)) {
+              case 0:
+                reader.beginArray();
+                while (reader.hasNext()) {
+                  shapes.add((ShapeGroup) ContentModelParser.parse(reader, composition));
+                }
+                reader.endArray();
+                break;
+              default:
+                reader.skipName();
+                reader.skipValue();
             }
           }
           reader.endObject();
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/FontParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/FontParser.java
index 9365bd5..d155db5 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/FontParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/FontParser.java
@@ -1,12 +1,17 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
-
 import com.airbnb.lottie.model.Font;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
 class FontParser {
+  private static final JsonReader.Options NAMES = JsonReader.Options.of(
+      "fFamily",
+      "fName",
+      "fStyle",
+      "ascent"
+  );
 
   private FontParser() {}
 
@@ -18,20 +23,21 @@
 
     reader.beginObject();
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "fFamily":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           family = reader.nextString();
           break;
-        case "fName":
+        case 1:
           name = reader.nextString();
           break;
-        case "fStyle":
+        case 2:
           style = reader.nextString();
           break;
-        case "ascent":
+        case 3:
           ascent = (float) reader.nextDouble();
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/GradientColorParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/GradientColorParser.java
index b330693..06ed6fa 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/GradientColorParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/GradientColorParser.java
@@ -1,19 +1,21 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.Color;
-import androidx.annotation.IntRange;
-import android.util.JsonReader;
-import android.util.JsonToken;
 
 import com.airbnb.lottie.model.content.GradientColor;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.utils.MiscUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
+import androidx.annotation.IntRange;
+
 public class GradientColorParser implements com.airbnb.lottie.parser.ValueParser<GradientColor> {
-  /** The number of colors if it exists in the json or -1 if it doesn't (legacy bodymovin) */
+  /**
+   * The number of colors if it exists in the json or -1 if it doesn't (legacy bodymovin)
+   */
   private int colorPoints;
 
   public GradientColorParser(int colorPoints) {
@@ -24,35 +26,36 @@
    * Both the color stops and opacity stops are in the same array.
    * There are {@link #colorPoints} colors sequentially as:
    * [
-   *     ...,
-   *     position,
-   *     red,
-   *     green,
-   *     blue,
-   *     ...
+   * ...,
+   * position,
+   * red,
+   * green,
+   * blue,
+   * ...
    * ]
-   *
+   * <p>
    * The remainder of the array is the opacity stops sequentially as:
    * [
-   *     ...,
-   *     position,
-   *     opacity,
-   *     ...
+   * ...,
+   * position,
+   * opacity,
+   * ...
    * ]
    */
-  @Override public GradientColor parse(JsonReader reader, float scale)
+  @Override
+  public GradientColor parse(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;
+    boolean isArray = reader.peek() == JsonReader.Token.BEGIN_ARRAY;
     if (isArray) {
       reader.beginArray();
     }
     while (reader.hasNext()) {
       array.add((float) reader.nextDouble());
     }
-    if(isArray) {
+    if (isArray) {
       reader.endArray();
     }
     if (colorPoints == -1) {
@@ -95,7 +98,7 @@
    * Opacity stops can be at arbitrary intervals independent of color stops.
    * This uses the existing color stops and modifies the opacity at each existing color stop
    * based on what the opacity would be.
-   *
+   * <p>
    * 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.
    */
@@ -130,7 +133,7 @@
     }
   }
 
-  @IntRange(from=0, to=255)
+  @IntRange(from = 0, to = 255)
   private int getOpacityAtPosition(double position, double[] positions, double[] opacities) {
     for (int i = 1; i < positions.length; i++) {
       double lastPosition = positions[i - 1];
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/GradientFillParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/GradientFillParser.java
index ad32645..9f69f43 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/GradientFillParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/GradientFillParser.java
@@ -1,18 +1,33 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.Path;
-import android.util.JsonReader;
 
+import android.webkit.JsResult;
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatableGradientColorValue;
 import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
 import com.airbnb.lottie.model.animatable.AnimatablePointValue;
 import com.airbnb.lottie.model.content.GradientFill;
 import com.airbnb.lottie.model.content.GradientType;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
 class GradientFillParser {
+  private static final JsonReader.Options NAMES = JsonReader.Options.of(
+      "nm",
+      "g",
+      "o",
+      "t",
+      "s",
+      "e",
+      "r",
+      "hd"
+  );
+  private static final JsonReader.Options GRADIENT_NAMES = JsonReader.Options.of(
+      "p",
+      "k"
+  );
 
   private GradientFillParser() {}
 
@@ -28,46 +43,48 @@
     boolean hidden = false;
 
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "nm":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           name = reader.nextString();
           break;
-        case "g":
+        case 1:
           int points = -1;
           reader.beginObject();
           while (reader.hasNext()) {
-            switch (reader.nextName()) {
-              case "p":
+            switch (reader.selectName(GRADIENT_NAMES)) {
+              case 0:
                 points = reader.nextInt();
                 break;
-              case "k":
+              case 1:
                 color = AnimatableValueParser.parseGradientColor(reader, composition, points);
                 break;
               default:
+                reader.skipName();
                 reader.skipValue();
             }
           }
           reader.endObject();
           break;
-        case "o":
+        case 2:
           opacity = AnimatableValueParser.parseInteger(reader, composition);
           break;
-        case "t":
+        case 3:
           gradientType = reader.nextInt() == 1 ? GradientType.LINEAR : GradientType.RADIAL;
           break;
-        case "s":
+        case 4:
           startPoint = AnimatableValueParser.parsePoint(reader, composition);
           break;
-        case "e":
+        case 5:
           endPoint = AnimatableValueParser.parsePoint(reader, composition);
           break;
-        case "r":
+        case 6:
           fillType = reader.nextInt() == 1 ? Path.FillType.WINDING : Path.FillType.EVEN_ODD;
           break;
-        case "hd":
+        case 7:
           hidden = reader.nextBoolean();
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/GradientStrokeParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/GradientStrokeParser.java
index 4773a93..69f61dc 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/GradientStrokeParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/GradientStrokeParser.java
@@ -1,6 +1,5 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
@@ -10,6 +9,7 @@
 import com.airbnb.lottie.model.content.GradientStroke;
 import com.airbnb.lottie.model.content.GradientType;
 import com.airbnb.lottie.model.content.ShapeStroke;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -18,6 +18,28 @@
 class GradientStrokeParser {
 
   private GradientStrokeParser() {}
+  private static JsonReader.Options NAMES = JsonReader.Options.of(
+      "nm",
+      "g",
+      "o",
+      "t",
+      "s",
+      "e",
+      "w",
+      "lc",
+      "lj",
+      "ml",
+      "hd",
+      "d"
+  );
+  private static final JsonReader.Options GRADIENT_NAMES = JsonReader.Options.of(
+      "p",
+      "k"
+  );
+  private static final JsonReader.Options DASH_PATTERN_NAMES = JsonReader.Options.of(
+      "n",
+      "v"
+  );
 
   static GradientStroke parse(
       JsonReader reader, LottieComposition composition) throws IOException {
@@ -38,69 +60,71 @@
     List<AnimatableFloatValue> lineDashPattern = new ArrayList<>();
 
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "nm":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           name = reader.nextString();
           break;
-        case "g":
+        case 1:
           int points = -1;
           reader.beginObject();
           while (reader.hasNext()) {
-            switch (reader.nextName()) {
-              case "p":
+            switch (reader.selectName(GRADIENT_NAMES)) {
+              case 0:
                 points = reader.nextInt();
                 break;
-              case "k":
+              case 1:
                 color = AnimatableValueParser.parseGradientColor(reader, composition, points);
                 break;
               default:
+                reader.skipName();
                 reader.skipValue();
             }
           }
           reader.endObject();
           break;
-        case "o":
+        case 2:
           opacity = AnimatableValueParser.parseInteger(reader, composition);
           break;
-        case "t":
+        case 3:
           gradientType = reader.nextInt() == 1 ? GradientType.LINEAR : GradientType.RADIAL;
           break;
-        case "s":
+        case 4:
           startPoint = AnimatableValueParser.parsePoint(reader, composition);
           break;
-        case "e":
+        case 5:
           endPoint = AnimatableValueParser.parsePoint(reader, composition);
           break;
-        case "w":
+        case 6:
           width = AnimatableValueParser.parseFloat(reader, composition);
           break;
-        case "lc":
+        case 7:
           capType = ShapeStroke.LineCapType.values()[reader.nextInt() - 1];
           break;
-        case "lj":
+        case 8:
           joinType = ShapeStroke.LineJoinType.values()[reader.nextInt() - 1];
           break;
-        case "ml":
+        case 9:
           miterLimit = (float) reader.nextDouble();
           break;
-        case "hd":
+        case 10:
           hidden = reader.nextBoolean();
           break;
-        case "d":
+        case 11:
           reader.beginArray();
           while (reader.hasNext()) {
             String n = null;
             AnimatableFloatValue val = null;
             reader.beginObject();
             while (reader.hasNext()) {
-              switch (reader.nextName()) {
-                case "n":
+              switch (reader.selectName(DASH_PATTERN_NAMES)) {
+                case 0:
                   n = reader.nextString();
                   break;
-                case "v":
+                case 1:
                   val = AnimatableValueParser.parseFloat(reader, composition);
                   break;
                 default:
+                  reader.skipName();
                   reader.skipValue();
               }
             }
@@ -120,6 +144,7 @@
           }
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/IntegerParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/IntegerParser.java
index 71aa459..2ed12a4 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/IntegerParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/IntegerParser.java
@@ -1,6 +1,6 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/JsonUtils.java b/lottie/src/main/java/com/airbnb/lottie/parser/JsonUtils.java
index 26b38dc..7cd7709 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/JsonUtils.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/JsonUtils.java
@@ -3,9 +3,10 @@
 import android.graphics.Color;
 import android.graphics.PointF;
 import androidx.annotation.ColorInt;
-import android.util.JsonReader;
 import android.util.JsonToken;
 
+import com.airbnb.lottie.parser.moshi.JsonReader;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -33,7 +34,7 @@
     List<PointF> points = new ArrayList<>();
 
     reader.beginArray();
-    while (reader.peek() == JsonToken.BEGIN_ARRAY) {
+    while (reader.peek() == JsonReader.Token.BEGIN_ARRAY) {
       reader.beginArray();
       points.add(jsonToPoint(reader, scale));
       reader.endArray();
@@ -66,26 +67,29 @@
     reader.beginArray();
     x = (float) reader.nextDouble();
     y = (float) reader.nextDouble();
-    while (reader.peek() != JsonToken.END_ARRAY) {
+    while (reader.peek() != JsonReader.Token.END_ARRAY) {
       reader.skipValue();
     }
     reader.endArray();
     return new PointF(x * scale, y * scale);
   }
 
+  private static final JsonReader.Options POINT_NAMES = JsonReader.Options.of("x", "y");
+
   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":
+      switch (reader.selectName(POINT_NAMES)) {
+        case 0:
           x = valueFromObject(reader);
           break;
-        case "y":
+        case 1:
           y = valueFromObject(reader);
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
@@ -94,7 +98,7 @@
   }
 
   static float valueFromObject(JsonReader reader) throws IOException {
-    JsonToken token = reader.peek();
+    JsonReader.Token token = reader.peek();
     switch (token) {
       case NUMBER:
         return (float) reader.nextDouble();
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/KeyframeParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/KeyframeParser.java
index 72ed856..8cd45ba 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/KeyframeParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/KeyframeParser.java
@@ -4,11 +4,11 @@
 import androidx.annotation.Nullable;
 import androidx.collection.SparseArrayCompat;
 import androidx.core.view.animation.PathInterpolatorCompat;
-import android.util.JsonReader;
 import android.view.animation.Interpolator;
 import android.view.animation.LinearInterpolator;
 
 import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.value.Keyframe;
 import com.airbnb.lottie.utils.MiscUtils;
 import com.airbnb.lottie.utils.Utils;
@@ -26,6 +26,16 @@
   private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
   private static SparseArrayCompat<WeakReference<Interpolator>> pathInterpolatorCache;
 
+  static JsonReader.Options NAMES = JsonReader.Options.of(
+      "t",
+      "s",
+      "e",
+      "o",
+      "i",
+      "h",
+      "to",
+      "ti"
+  );
   // https://github.com/airbnb/lottie-android/issues/464
   private static SparseArrayCompat<WeakReference<Interpolator>> pathInterpolatorCache() {
     if (pathInterpolatorCache == null) {
@@ -52,7 +62,7 @@
   }
 
   static <T> Keyframe<T> parse(JsonReader reader, LottieComposition composition,
-      float scale, ValueParser<T> valueParser, boolean animated) throws IOException {
+                               float scale, ValueParser<T> valueParser, boolean animated) throws IOException {
 
     if (animated) {
       return parseKeyframe(composition, reader, scale, valueParser);
@@ -81,29 +91,29 @@
 
     reader.beginObject();
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "t":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           startFrame = (float) reader.nextDouble();
           break;
-        case "s":
+        case 1:
           startValue = valueParser.parse(reader, scale);
           break;
-        case "e":
+        case 2:
           endValue = valueParser.parse(reader, scale);
           break;
-        case "o":
+        case 3:
           cp1 = JsonUtils.jsonToPoint(reader, scale);
           break;
-        case "i":
+        case 4:
           cp2 = JsonUtils.jsonToPoint(reader, scale);
           break;
-        case "h":
+        case 5:
           hold = reader.nextInt() == 1;
           break;
-        case "to":
+        case 6:
           pathCp1 = JsonUtils.jsonToPoint(reader, scale);
           break;
-        case "ti":
+        case 7:
           pathCp2 = JsonUtils.jsonToPoint(reader, scale);
           break;
         default:
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/KeyframesParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/KeyframesParser.java
index 24d91bc..b3f2c5c 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/KeyframesParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/KeyframesParser.java
@@ -1,10 +1,8 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
-import android.util.JsonToken;
-
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.animation.keyframe.PathKeyframe;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.value.Keyframe;
 
 import java.io.IOException;
@@ -13,26 +11,29 @@
 
 class KeyframesParser {
 
-  private KeyframesParser() {}
+  static JsonReader.Options NAMES = JsonReader.Options.of("k");
+
+  private KeyframesParser() {
+  }
 
   static <T> List<Keyframe<T>> parse(JsonReader reader,
-      LottieComposition composition, float scale, ValueParser<T> valueParser)
+                                     LottieComposition composition, float scale, ValueParser<T> valueParser)
       throws IOException {
     List<Keyframe<T>> keyframes = new ArrayList<>();
 
-    if (reader.peek() == JsonToken.STRING) {
+    if (reader.peek() == JsonReader.Token.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) {
+      switch (reader.selectName(NAMES)) {
+        case 0:
+          if (reader.peek() == JsonReader.Token.BEGIN_ARRAY) {
             reader.beginArray();
 
-            if (reader.peek() == JsonToken.NUMBER) {
+            if (reader.peek() == JsonReader.Token.NUMBER) {
               // For properties in which the static value is an array of numbers.
               keyframes.add(KeyframeParser.parse(reader, composition, scale, valueParser, false));
             } else {
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java
index 7f6a1c1..a066b20 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/LayerParser.java
@@ -2,9 +2,9 @@
 
 import android.graphics.Color;
 import android.graphics.Rect;
-import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.value.Keyframe;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
 import com.airbnb.lottie.model.animatable.AnimatableTextFrame;
@@ -24,6 +24,32 @@
 
   private LayerParser() {}
 
+  private static final JsonReader.Options NAMES = JsonReader.Options.of(
+      "nm", // 0
+      "ind", // 1
+      "refId", // 2
+      "ty", // 3
+      "parent", // 4
+      "sw", // 5
+      "sh", // 6
+      "sc", // 7
+      "ks", // 8
+      "tt", // 9
+      "masksProperties", // 10
+      "shapes", // 11
+      "t", // 12
+      "ef", // 13
+      "sr", // 14
+      "st", // 15
+      "w", // 16
+      "h", // 17
+      "ip", // 18
+      "op", // 19
+      "tm", // 20
+      "cl", // 21
+      "hd" // 22
+  );
+
   public static Layer parse(LottieComposition composition) {
     Rect bounds = composition.getBounds();
     return new Layer(
@@ -34,6 +60,13 @@
         Layer.MatteType.NONE, null, false);
   }
 
+  private static final JsonReader.Options TEXT_NAMES = JsonReader.Options.of(
+      "d",
+      "a"
+  );
+
+  private static final JsonReader.Options EFFECTS_NAMES = JsonReader.Options.of("nm");
+
   public static Layer parse(JsonReader reader, LottieComposition composition) throws IOException {
     // This should always be set by After Effects. However, if somebody wants to minify
     // and optimize their json, the name isn't critical for most cases so it can be removed.
@@ -65,17 +98,17 @@
 
     reader.beginObject();
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "nm":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           layerName = reader.nextString();
           break;
-        case "ind":
+        case 1:
           layerId = reader.nextInt();
           break;
-        case "refId":
+        case 2:
           refId = reader.nextString();
           break;
-        case "ty":
+        case 3:
           int layerTypeInt = reader.nextInt();
           if (layerTypeInt < Layer.LayerType.UNKNOWN.ordinal()) {
             layerType = Layer.LayerType.values()[layerTypeInt];
@@ -83,26 +116,26 @@
             layerType = Layer.LayerType.UNKNOWN;
           }
           break;
-        case "parent":
+        case 4:
           parentId = reader.nextInt();
           break;
-        case "sw":
+        case 5:
           solidWidth = (int) (reader.nextInt() * Utils.dpScale());
           break;
-        case "sh":
+        case 6:
           solidHeight = (int) (reader.nextInt() * Utils.dpScale());
           break;
-        case "sc":
+        case 7:
           solidColor = Color.parseColor(reader.nextString());
           break;
-        case "ks":
+        case 8:
           transform = AnimatableTransformParser.parse(reader, composition);
           break;
-        case "tt":
+        case 9:
           matteType = Layer.MatteType.values()[reader.nextInt()];
           composition.incrementMatteOrMaskCount(1);
           break;
-        case "masksProperties":
+        case 10:
           reader.beginArray();
           while (reader.hasNext()) {
             masks.add(MaskParser.parse(reader, composition));
@@ -110,7 +143,7 @@
           composition.incrementMatteOrMaskCount(masks.size());
           reader.endArray();
           break;
-        case "shapes":
+        case 11:
           reader.beginArray();
           while (reader.hasNext()) {
             ContentModel shape = ContentModelParser.parse(reader, composition);
@@ -120,14 +153,14 @@
           }
           reader.endArray();
           break;
-        case "t":
+        case 12:
           reader.beginObject();
           while (reader.hasNext()) {
-            switch (reader.nextName()) {
-              case "d":
+            switch (reader.selectName(TEXT_NAMES)) {
+              case 0:
                 text = AnimatableValueParser.parseDocumentData(reader, composition);
                 break;
-              case "a":
+              case 1:
                 reader.beginArray();
                 if (reader.hasNext()) {
                   textProperties = AnimatableTextPropertiesParser.parse(reader, composition);
@@ -138,22 +171,24 @@
                 reader.endArray();
                 break;
               default:
+                reader.skipName();
                 reader.skipValue();
             }
           }
           reader.endObject();
           break;
-        case "ef":
+        case 13:
           reader.beginArray();
           List<String> effectNames = new ArrayList<>();
           while (reader.hasNext()) {
             reader.beginObject();
             while (reader.hasNext()) {
-              switch (reader.nextName()) {
-                case "nm":
+              switch (reader.selectName(EFFECTS_NAMES)) {
+                case 0:
                   effectNames.add(reader.nextString());
                   break;
                 default:
+                  reader.skipName();
                   reader.skipValue();
 
               }
@@ -165,34 +200,35 @@
               " fills, strokes, trim paths etc. then try adding them directly as contents " +
               " in your shape. Found: " + effectNames);
           break;
-        case "sr":
+        case 14:
           timeStretch = (float) reader.nextDouble();
           break;
-        case "st":
+        case 15:
           startFrame = (float) reader.nextDouble();
           break;
-        case "w":
+        case 16:
           preCompWidth = (int) (reader.nextInt() * Utils.dpScale());
           break;
-        case "h":
+        case 17:
           preCompHeight = (int) (reader.nextInt() * Utils.dpScale());
           break;
-        case "ip":
+        case 18:
           inFrame = (float) reader.nextDouble();
           break;
-        case "op":
+        case 19:
           outFrame = (float) reader.nextDouble();
           break;
-        case "tm":
+        case 20:
           timeRemapping = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
-        case "cl":
+        case 21:
           cl = reader.nextString();
           break;
-        case "hd":
+        case 22:
           hidden = reader.nextBoolean();
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionMoshiParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionMoshiParser.java
new file mode 100644
index 0000000..3ff3e24
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionMoshiParser.java
@@ -0,0 +1,272 @@
+package com.airbnb.lottie.parser;
+
+import android.graphics.Rect;
+
+import com.airbnb.lottie.L;
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.LottieImageAsset;
+import com.airbnb.lottie.model.Font;
+import com.airbnb.lottie.model.FontCharacter;
+import com.airbnb.lottie.model.Marker;
+import com.airbnb.lottie.model.layer.Layer;
+import com.airbnb.lottie.parser.moshi.JsonReader;
+import com.airbnb.lottie.utils.Logger;
+import com.airbnb.lottie.utils.Utils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import androidx.collection.LongSparseArray;
+import androidx.collection.SparseArrayCompat;
+
+
+public class LottieCompositionMoshiParser {
+  private static final JsonReader.Options NAMES = JsonReader.Options.of(
+      "w", // 0
+      "h", // 1
+      "ip", // 2
+      "op", // 3
+      "fr", // 4
+      "v", // 5
+      "layers", // 6
+      "assets", // 7
+      "fonts", // 8
+      "chars", // 9
+      "markers" // 10
+  );
+
+  public static LottieComposition parse(JsonReader reader) throws IOException {
+    float scale = Utils.dpScale();
+    float startFrame = 0f;
+    float endFrame = 0f;
+    float frameRate = 0f;
+    final LongSparseArray<Layer> layerMap = new LongSparseArray<>();
+    final List<Layer> layers = new ArrayList<>();
+    int width = 0;
+    int height = 0;
+    Map<String, List<Layer>> precomps = new HashMap<>();
+    Map<String, LottieImageAsset> images = new HashMap<>();
+    Map<String, Font> fonts = new HashMap<>();
+    List<Marker> markers = new ArrayList<>();
+    SparseArrayCompat<FontCharacter> characters = new SparseArrayCompat<>();
+
+    LottieComposition composition = new LottieComposition();
+    reader.beginObject();
+    while (reader.hasNext()) {
+      switch (reader.selectName(NAMES)) {
+        case 0:
+          width = reader.nextInt();
+          break;
+        case 1:
+          height = reader.nextInt();
+          break;
+        case 2:
+          startFrame = (float) reader.nextDouble();
+          break;
+        case 3:
+          endFrame = (float) reader.nextDouble() - 0.01f;
+          break;
+        case 4:
+          frameRate = (float) reader.nextDouble();
+          break;
+        case 5:
+          String version = reader.nextString();
+          String[] versions = version.split("\\.");
+          int majorVersion = Integer.parseInt(versions[0]);
+          int minorVersion = Integer.parseInt(versions[1]);
+          int patchVersion = Integer.parseInt(versions[2]);
+          if (!Utils.isAtLeastVersion(majorVersion, minorVersion, patchVersion,
+              4, 4, 0)) {
+            composition.addWarning("Lottie only supports bodymovin >= 4.4.0");
+          }
+          break;
+        case 6:
+          parseLayers(reader, composition, layers, layerMap);
+          break;
+        case 7:
+          parseAssets(reader, composition, precomps, images);
+          break;
+        case 8:
+          parseFonts(reader, fonts);
+          break;
+        case 9:
+          parseChars(reader, composition, characters);
+          break;
+        case 10:
+          parseMarkers(reader, composition, markers);
+          break;
+        default:
+          reader.skipName();
+          reader.skipValue();
+      }
+    }
+    int scaledWidth = (int) (width * scale);
+    int scaledHeight = (int) (height * scale);
+    Rect bounds = new Rect(0, 0, scaledWidth, scaledHeight);
+
+    composition.init(bounds, startFrame, endFrame, frameRate, layers, layerMap, precomps,
+        images, characters, fonts, markers);
+
+    return composition;
+  }
+
+  private static void parseLayers(JsonReader reader, LottieComposition composition,
+                                  List<Layer> layers, LongSparseArray<Layer> layerMap) throws IOException {
+    int imageCount = 0;
+    reader.beginArray();
+    while (reader.hasNext()) {
+      Layer layer = LayerParser.parse(reader, composition);
+      if (layer.getLayerType() == Layer.LayerType.IMAGE) {
+        imageCount++;
+      }
+      layers.add(layer);
+      layerMap.put(layer.getId(), layer);
+
+      if (imageCount > 4) {
+        Logger.warning("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();
+  }
+
+
+  static JsonReader.Options ASSETS_NAMES = JsonReader.Options.of(
+      "id", // 0
+      "layers", // 1
+      "w", // 2
+      "h", // 3
+      "p", // 4
+      "u" // 5
+  );
+
+  private static void parseAssets(JsonReader reader, LottieComposition composition,
+      Map<String, List<Layer>> precomps, Map<String, LottieImageAsset> images) throws IOException {
+    reader.beginArray();
+    while (reader.hasNext()) {
+      String id = null;
+      // For precomps
+      List<Layer> layers = new ArrayList<>();
+      LongSparseArray<Layer> layerMap = new LongSparseArray<>();
+      // For images
+      int width = 0;
+      int height = 0;
+      String imageFileName = null;
+      String relativeFolder = null;
+      reader.beginObject();
+      while (reader.hasNext()) {
+        switch (reader.selectName(ASSETS_NAMES)) {
+          case 0:
+            id = reader.nextString();
+            break;
+          case 1:
+            reader.beginArray();
+            while (reader.hasNext()) {
+              Layer layer = LayerParser.parse(reader, composition);
+              layerMap.put(layer.getId(), layer);
+              layers.add(layer);
+            }
+            reader.endArray();
+            break;
+          case 2:
+            width = reader.nextInt();
+            break;
+          case 3:
+            height = reader.nextInt();
+            break;
+          case 4:
+            imageFileName = reader.nextString();
+            break;
+          case 5:
+            relativeFolder = reader.nextString();
+            break;
+          default:
+            reader.skipName();
+            reader.skipValue();
+        }
+      }
+      reader.endObject();
+      if (imageFileName != null) {
+        LottieImageAsset image =
+            new LottieImageAsset(width, height, id, imageFileName, relativeFolder);
+        images.put(image.getId(), image);
+      } else {
+        precomps.put(id, layers);
+      }
+    }
+    reader.endArray();
+  }
+
+  private static final JsonReader.Options FONT_NAMES = JsonReader.Options.of("list");
+
+  private static void parseFonts(JsonReader reader, Map<String, Font> fonts) throws IOException {
+    reader.beginObject();
+    while (reader.hasNext()) {
+      switch (reader.selectName(FONT_NAMES)) {
+        case 0:
+          reader.beginArray();
+          while (reader.hasNext()) {
+            Font font = FontParser.parse(reader);
+            fonts.put(font.getName(), font);
+          }
+          reader.endArray();
+          break;
+        default:
+          reader.skipName();
+          reader.skipValue();
+      }
+    }
+    reader.endObject();
+  }
+
+  private static void parseChars(
+      JsonReader reader, LottieComposition composition,
+      SparseArrayCompat<FontCharacter> characters) throws IOException {
+    reader.beginArray();
+    while (reader.hasNext()) {
+      FontCharacter character = FontCharacterParser.parse(reader, composition);
+      characters.put(character.hashCode(), character);
+    }
+    reader.endArray();
+  }
+
+  private static final JsonReader.Options MARKER_NAMES = JsonReader.Options.of(
+      "cm",
+      "tm",
+      "dr"
+  );
+
+  private static void parseMarkers(
+      JsonReader reader, LottieComposition composition, List<Marker> markers) throws IOException{
+    reader.beginArray();
+    while (reader.hasNext()) {
+      String comment = null;
+      float frame = 0f;
+      float durationFrames = 0f;
+      reader.beginObject();
+      while (reader.hasNext()) {
+        switch (reader.selectName(MARKER_NAMES)) {
+          case 0:
+            comment = reader.nextString();
+            break;
+          case 1:
+            frame = (float) reader.nextDouble();
+            break;
+          case 2:
+            durationFrames = (float) reader.nextDouble();
+            break;
+          default:
+            reader.skipName();
+            reader.skipValue();
+        }
+      }
+      reader.endObject();
+      markers.add(new Marker(comment, frame, durationFrames));
+    }
+    reader.endArray();
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionParser.java
index afe8f2d..03e8eac 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionParser.java
@@ -1,9 +1,6 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.Rect;
-import androidx.collection.LongSparseArray;
-import androidx.collection.SparseArrayCompat;
-import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieImageAsset;
@@ -11,6 +8,7 @@
 import com.airbnb.lottie.model.FontCharacter;
 import com.airbnb.lottie.model.Marker;
 import com.airbnb.lottie.model.layer.Layer;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.utils.Logger;
 import com.airbnb.lottie.utils.Utils;
 
@@ -20,9 +18,23 @@
 import java.util.List;
 import java.util.Map;
 
-public class LottieCompositionParser {
+import androidx.collection.LongSparseArray;
+import androidx.collection.SparseArrayCompat;
 
-  private LottieCompositionParser() {}
+
+public class LottieCompositionParser {
+  static JsonReader.Options NAMES = JsonReader.Options.of(
+      "w",
+      "h",
+      "ip",
+      "op",
+      "fr",
+      "v",
+      "layers",
+      "assets",
+      "fonts",
+      "chars",
+      "markers");
 
   public static LottieComposition parse(JsonReader reader) throws IOException {
     float scale = Utils.dpScale();
@@ -40,26 +52,25 @@
     SparseArrayCompat<FontCharacter> characters = new SparseArrayCompat<>();
 
     LottieComposition composition = new LottieComposition();
-
     reader.beginObject();
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "w":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           width = reader.nextInt();
           break;
-        case "h":
+        case 1:
           height = reader.nextInt();
           break;
-        case "ip":
+        case 2:
           startFrame = (float) reader.nextDouble();
           break;
-        case "op":
+        case 3:
           endFrame = (float) reader.nextDouble() - 0.01f;
           break;
-        case "fr":
+        case 4:
           frameRate = (float) reader.nextDouble();
           break;
-        case "v":
+        case 5:
           String version = reader.nextString();
           String[] versions = version.split("\\.");
           int majorVersion = Integer.parseInt(versions[0]);
@@ -70,27 +81,13 @@
             composition.addWarning("Lottie only supports bodymovin >= 4.4.0");
           }
           break;
-        case "layers":
+        case 6:
           parseLayers(reader, composition, layers, layerMap);
-          break;
-        case "assets":
-          parseAssets(reader, composition, precomps, images);
-          break;
-        case "fonts":
-          parseFonts(reader, fonts);
-          break;
-        case "chars":
-          parseChars(reader, composition, characters);
-          break;
-        case "markers":
-          parseMarkers(reader, composition, markers);
-          break;
         default:
           reader.skipValue();
+
       }
     }
-    reader.endObject();
-
     int scaledWidth = (int) (width * scale);
     int scaledHeight = (int) (height * scale);
     Rect bounds = new Rect(0, 0, scaledWidth, scaledHeight);
@@ -102,7 +99,7 @@
   }
 
   private static void parseLayers(JsonReader reader, LottieComposition composition,
-      List<Layer> layers, LongSparseArray<Layer> layerMap) throws IOException {
+                                  List<Layer> layers, LongSparseArray<Layer> layerMap) throws IOException {
     int imageCount = 0;
     reader.beginArray();
     while (reader.hasNext()) {
@@ -121,119 +118,4 @@
     }
     reader.endArray();
   }
-
-  private static void parseAssets(JsonReader reader, LottieComposition composition,
-      Map<String, List<Layer>> precomps, Map<String, LottieImageAsset> images) throws IOException {
-    reader.beginArray();
-    while (reader.hasNext()) {
-      String id = null;
-      // For precomps
-      List<Layer> layers = new ArrayList<>();
-      LongSparseArray<Layer> layerMap = new LongSparseArray<>();
-      // 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 = LayerParser.parse(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();
-        }
-      }
-      reader.endObject();
-      if (imageFileName != null) {
-        LottieImageAsset image =
-            new LottieImageAsset(width, height, id, imageFileName, relativeFolder);
-        images.put(image.getId(), image);
-      } else {
-        precomps.put(id, layers);
-      }
-    }
-    reader.endArray();
-  }
-
-  private static void parseFonts(JsonReader reader, Map<String, Font> fonts) throws IOException {
-    reader.beginObject();
-    while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "list":
-          reader.beginArray();
-          while (reader.hasNext()) {
-            Font font = FontParser.parse(reader);
-            fonts.put(font.getName(), font);
-          }
-          reader.endArray();
-          break;
-        default:
-          reader.skipValue();
-      }
-    }
-    reader.endObject();
-  }
-
-  private static void parseChars(
-      JsonReader reader, LottieComposition composition,
-      SparseArrayCompat<FontCharacter> characters) throws IOException {
-    reader.beginArray();
-    while (reader.hasNext()) {
-      FontCharacter character = FontCharacterParser.parse(reader, composition);
-      characters.put(character.hashCode(), character);
-    }
-    reader.endArray();
-  }
-
-  private static void parseMarkers(
-      JsonReader reader, LottieComposition composition, List<Marker> markers) throws IOException{
-    reader.beginArray();
-    while (reader.hasNext()) {
-      String comment = null;
-      float frame = 0f;
-      float durationFrames = 0f;
-      reader.beginObject();
-      while (reader.hasNext()) {
-        switch (reader.nextName()) {
-          case "cm":
-            comment = reader.nextString();
-            break;
-          case "tm":
-            frame = (float) reader.nextDouble();
-            break;
-          case "dr":
-            durationFrames = (float) reader.nextDouble();
-            break;
-          default:
-            reader.skipValue();
-        }
-      }
-      reader.endObject();
-      markers.add(new Marker(comment, frame, durationFrames));
-    }
-    reader.endArray();
-  }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/MaskParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/MaskParser.java
index 108bcac..97c75e5 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/MaskParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/MaskParser.java
@@ -1,11 +1,10 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
-
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
 import com.airbnb.lottie.model.animatable.AnimatableShapeValue;
 import com.airbnb.lottie.model.content.Mask;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.utils.Logger;
 
 import java.io.IOException;
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/MergePathsParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/MergePathsParser.java
index 6d6569d..7f52fd2 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/MergePathsParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/MergePathsParser.java
@@ -1,12 +1,16 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
-
 import com.airbnb.lottie.model.content.MergePaths;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
 class MergePathsParser {
+  private static final JsonReader.Options NAMES = JsonReader.Options.of(
+      "nm",
+      "mm",
+      "hd"
+  );
 
   private MergePathsParser() {}
 
@@ -16,17 +20,18 @@
     boolean hidden = false;
 
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "nm":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           name = reader.nextString();
           break;
-        case "mm":
+        case 1:
           mode =  MergePaths.MergePathsMode.forId(reader.nextInt());
           break;
-        case "hd":
+        case 2:
           hidden = reader.nextBoolean();
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/PathKeyframeParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/PathKeyframeParser.java
index a702ff6..3752b92 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/PathKeyframeParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/PathKeyframeParser.java
@@ -1,10 +1,10 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.PointF;
-import android.util.JsonReader;
 import android.util.JsonToken;
 
 import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.value.Keyframe;
 import com.airbnb.lottie.animation.keyframe.PathKeyframe;
 import com.airbnb.lottie.utils.Utils;
@@ -17,7 +17,7 @@
 
   static PathKeyframe parse(
       JsonReader reader, LottieComposition composition) throws IOException {
-    boolean animated = reader.peek() == JsonToken.BEGIN_OBJECT;
+    boolean animated = reader.peek() == JsonReader.Token.BEGIN_OBJECT;
     Keyframe<PointF> keyframe = KeyframeParser.parse(
         reader, composition, Utils.dpScale(), PathParser.INSTANCE, animated);
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/PathParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/PathParser.java
index 72f87a4..0141725 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/PathParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/PathParser.java
@@ -1,7 +1,8 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.PointF;
-import android.util.JsonReader;
+
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/PointFParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/PointFParser.java
index ed832b2..9492360 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/PointFParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/PointFParser.java
@@ -1,23 +1,25 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.PointF;
-import android.util.JsonReader;
-import android.util.JsonToken;
+
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
 public class PointFParser implements ValueParser<PointF> {
   public static final PointFParser INSTANCE = new PointFParser();
 
-  private PointFParser() {}
+  private PointFParser() {
+  }
 
-  @Override public PointF parse(JsonReader reader, float scale) throws IOException {
-    JsonToken token = reader.peek();
-    if (token == JsonToken.BEGIN_ARRAY) {
+  @Override
+  public PointF parse(JsonReader reader, float scale) throws IOException {
+    JsonReader.Token token = reader.peek();
+    if (token == JsonReader.Token.BEGIN_ARRAY) {
       return JsonUtils.jsonToPoint(reader, scale);
-    } else if (token == JsonToken.BEGIN_OBJECT) {
+    } else if (token == JsonReader.Token.BEGIN_OBJECT) {
       return JsonUtils.jsonToPoint(reader, scale);
-    } else if (token == JsonToken.NUMBER) {
+    } else if (token == JsonReader.Token.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.
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/PolystarShapeParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/PolystarShapeParser.java
index 0dbf824..57678aa 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/PolystarShapeParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/PolystarShapeParser.java
@@ -1,16 +1,28 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.PointF;
-import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
 import com.airbnb.lottie.model.animatable.AnimatableValue;
 import com.airbnb.lottie.model.content.PolystarShape;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
 class PolystarShapeParser {
+  private static final JsonReader.Options NAMES = JsonReader.Options.of(
+    "nm",
+    "sy",
+    "pt",
+    "p",
+    "r",
+    "or",
+    "os",
+    "ir",
+    "is",
+    "hd"
+  );
 
   private PolystarShapeParser() {}
 
@@ -28,38 +40,39 @@
     boolean hidden = false;
 
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "nm":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           name = reader.nextString();
           break;
-        case "sy":
+        case 1:
           type = PolystarShape.Type.forValue(reader.nextInt());
           break;
-        case "pt":
+        case 2:
           points = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
-        case "p":
+        case 3:
           position = AnimatablePathValueParser.parseSplitPath(reader, composition);
           break;
-        case "r":
+        case 4:
           rotation = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
-        case "or":
+        case 5:
           outerRadius = AnimatableValueParser.parseFloat(reader, composition);
           break;
-        case "os":
+        case 6:
           outerRoundedness = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
-        case "ir":
+        case 7:
           innerRadius = AnimatableValueParser.parseFloat(reader, composition);
           break;
-        case "is":
+        case 8:
           innerRoundedness = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
-        case "hd":
+        case 9:
           hidden = reader.nextBoolean();
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/RectangleShapeParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/RectangleShapeParser.java
index af21210..0080ba6 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/RectangleShapeParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/RectangleShapeParser.java
@@ -1,19 +1,28 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.PointF;
-import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
 import com.airbnb.lottie.model.animatable.AnimatablePointValue;
 import com.airbnb.lottie.model.animatable.AnimatableValue;
 import com.airbnb.lottie.model.content.RectangleShape;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
 class RectangleShapeParser {
 
-  private RectangleShapeParser() {}
+  private static JsonReader.Options NAMES = JsonReader.Options.of(
+      "nm",
+      "p",
+      "s",
+      "r",
+      "hd"
+  );
+
+  private RectangleShapeParser() {
+  }
 
   static RectangleShape parse(
       JsonReader reader, LottieComposition composition) throws IOException {
@@ -24,21 +33,21 @@
     boolean hidden = false;
 
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "nm":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           name = reader.nextString();
           break;
-        case "p":
+        case 1:
           position =
               AnimatablePathValueParser.parseSplitPath(reader, composition);
           break;
-        case "s":
+        case 2:
           size = AnimatableValueParser.parsePoint(reader, composition);
           break;
-        case "r":
+        case 3:
           roundedness = AnimatableValueParser.parseFloat(reader, composition);
           break;
-        case "hd":
+        case 4:
           hidden = reader.nextBoolean();
           break;
         default:
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/RepeaterParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/RepeaterParser.java
index 0b877df..ba0d10d 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/RepeaterParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/RepeaterParser.java
@@ -1,17 +1,26 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
 import com.airbnb.lottie.model.animatable.AnimatableTransform;
 import com.airbnb.lottie.model.content.Repeater;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
 class RepeaterParser {
 
-  private RepeaterParser() {}
+  private static JsonReader.Options NAMES = JsonReader.Options.of(
+      "nm",
+      "c",
+      "o",
+      "tr",
+      "hd"
+  );
+
+  private RepeaterParser() {
+  }
 
   static Repeater parse(
       JsonReader reader, LottieComposition composition) throws IOException {
@@ -22,20 +31,20 @@
     boolean hidden = false;
 
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "nm":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           name = reader.nextString();
           break;
-        case "c":
+        case 1:
           copies = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
-        case "o":
+        case 2:
           offset = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
-        case "tr":
+        case 3:
           transform = AnimatableTransformParser.parse(reader, composition);
           break;
-        case "hd":
+        case 4:
           hidden = reader.nextBoolean();
           break;
         default:
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ScaleXYParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ScaleXYParser.java
index 91d6f87..e27e5a2 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/ScaleXYParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ScaleXYParser.java
@@ -1,8 +1,8 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
 import android.util.JsonToken;
 
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.value.ScaleXY;
 
 import java.io.IOException;
@@ -14,7 +14,7 @@
   }
 
   @Override public ScaleXY parse(JsonReader reader, float scale) throws IOException {
-    boolean isArray = reader.peek() == JsonToken.BEGIN_ARRAY;
+    boolean isArray = reader.peek() == JsonReader.Token.BEGIN_ARRAY;
     if (isArray) {
       reader.beginArray();
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeDataParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeDataParser.java
index 3d68bac..87f2e9c 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeDataParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeDataParser.java
@@ -1,11 +1,10 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.PointF;
-import android.util.JsonReader;
-import android.util.JsonToken;
 
 import com.airbnb.lottie.model.CubicCurveData;
 import com.airbnb.lottie.model.content.ShapeData;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 import com.airbnb.lottie.utils.MiscUtils;
 
 import java.io.IOException;
@@ -15,13 +14,21 @@
 
 public class ShapeDataParser implements ValueParser<ShapeData> {
   public static final ShapeDataParser INSTANCE = new ShapeDataParser();
+  private static final JsonReader.Options NAMES = JsonReader.Options.of(
+    "c",
+    "v",
+    "i",
+    "o"
+  );
 
-  private ShapeDataParser() {}
+  private ShapeDataParser() {
+  }
 
-  @Override public ShapeData parse(JsonReader reader, float scale) throws IOException {
+  @Override
+  public ShapeData parse(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) {
+    if (reader.peek() == JsonReader.Token.BEGIN_ARRAY) {
       reader.beginArray();
     }
 
@@ -32,25 +39,28 @@
     reader.beginObject();
 
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "c":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           closed = reader.nextBoolean();
           break;
-        case "v":
-          pointsArray =  JsonUtils.jsonToPoints(reader, scale);
+        case 1:
+          pointsArray = JsonUtils.jsonToPoints(reader, scale);
           break;
-        case "i":
-          inTangents =  JsonUtils.jsonToPoints(reader, scale);
+        case 2:
+          inTangents = JsonUtils.jsonToPoints(reader, scale);
           break;
-        case "o":
-          outTangents =  JsonUtils.jsonToPoints(reader, scale);
+        case 3:
+          outTangents = JsonUtils.jsonToPoints(reader, scale);
           break;
+        default:
+          reader.skipName();
+          reader.skipValue();
       }
     }
 
     reader.endObject();
 
-    if (reader.peek() == JsonToken.END_ARRAY) {
+    if (reader.peek() == JsonReader.Token.END_ARRAY) {
       reader.endArray();
     }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeFillParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeFillParser.java
index 0aaf8e0..fd4cef6 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeFillParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeFillParser.java
@@ -1,18 +1,26 @@
 package com.airbnb.lottie.parser;
 
 import android.graphics.Path;
-import android.util.JsonReader;
-
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatableColorValue;
 import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
 import com.airbnb.lottie.model.content.ShapeFill;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
 class ShapeFillParser {
+  private static final JsonReader.Options NAMES = JsonReader.Options.of(
+      "nm",
+      "c",
+      "o",
+      "fillEnabled",
+      "r",
+      "hd"
+  );
 
-  private ShapeFillParser() {}
+  private ShapeFillParser() {
+  }
 
   static ShapeFill parse(
       JsonReader reader, LottieComposition composition) throws IOException {
@@ -24,26 +32,27 @@
     boolean hidden = false;
 
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "nm":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           name = reader.nextString();
           break;
-        case "c":
+        case 1:
           color = AnimatableValueParser.parseColor(reader, composition);
           break;
-        case "o":
+        case 2:
           opacity = AnimatableValueParser.parseInteger(reader, composition);
           break;
-        case "fillEnabled":
+        case 3:
           fillEnabled = reader.nextBoolean();
           break;
-        case "r":
+        case 4:
           fillTypeInt = reader.nextInt();
           break;
-        case "hd":
+        case 5:
           hidden = reader.nextBoolean();
           break;
         default:
+          reader.skipName();
           reader.skipValue();
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeGroupParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeGroupParser.java
index eb3bed9..17eb682 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeGroupParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeGroupParser.java
@@ -1,10 +1,10 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.content.ContentModel;
 import com.airbnb.lottie.model.content.ShapeGroup;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -13,7 +13,11 @@
 class ShapeGroupParser {
 
   private ShapeGroupParser() {}
-
+  private static JsonReader.Options NAMES = JsonReader.Options.of(
+      "nm",
+      "hd",
+      "it"
+  );
   static ShapeGroup parse(
       JsonReader reader, LottieComposition composition) throws IOException {
     String name = null;
@@ -21,14 +25,14 @@
     List<ContentModel> items = new ArrayList<>();
 
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "nm":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           name = reader.nextString();
           break;
-        case "hd":
+        case 1:
           hidden = reader.nextBoolean();
           break;
-        case "it":
+        case 2:
           reader.beginArray();
           while (reader.hasNext()) {
             ContentModel newItem = ContentModelParser.parse(reader, composition);
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ShapePathParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ShapePathParser.java
index 4320012..26b7a94 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/ShapePathParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ShapePathParser.java
@@ -1,16 +1,24 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatableShapeValue;
 import com.airbnb.lottie.model.content.ShapePath;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
 class ShapePathParser {
 
-  private ShapePathParser() {}
+  static JsonReader.Options NAMES = JsonReader.Options.of(
+      "nm",
+      "ind",
+      "ks",
+      "hd"
+  );
+
+  private ShapePathParser() {
+  }
 
   static ShapePath parse(
       JsonReader reader, LottieComposition composition) throws IOException {
@@ -20,17 +28,17 @@
     boolean hidden = false;
 
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "nm":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           name = reader.nextString();
           break;
-        case "ind":
+        case 1:
           ind = reader.nextInt();
           break;
-        case "ks":
+        case 2:
           shape = AnimatableValueParser.parseShapeData(reader, composition);
           break;
-        case "hd":
+        case 3:
           hidden = reader.nextBoolean();
           break;
         default:
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeStrokeParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeStrokeParser.java
index b81bc06..a9b0c81 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeStrokeParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeStrokeParser.java
@@ -1,12 +1,12 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatableColorValue;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
 import com.airbnb.lottie.model.animatable.AnimatableIntegerValue;
 import com.airbnb.lottie.model.content.ShapeStroke;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -14,7 +14,24 @@
 
 class ShapeStrokeParser {
 
-  private ShapeStrokeParser() {}
+  private static JsonReader.Options NAMES = JsonReader.Options.of(
+      "nm",
+      "c",
+      "w",
+      "o",
+      "lc",
+      "lj",
+      "ml",
+      "hd",
+      "d"
+  );
+  private static final JsonReader.Options DASH_PATTERN_NAMES = JsonReader.Options.of(
+      "n",
+      "v"
+  );
+
+  private ShapeStrokeParser() {
+  }
 
   static ShapeStroke parse(
       JsonReader reader, LottieComposition composition) throws IOException {
@@ -31,32 +48,32 @@
     List<AnimatableFloatValue> lineDashPattern = new ArrayList<>();
 
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "nm":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           name = reader.nextString();
           break;
-        case "c":
+        case 1:
           color = AnimatableValueParser.parseColor(reader, composition);
           break;
-        case "w":
+        case 2:
           width = AnimatableValueParser.parseFloat(reader, composition);
           break;
-        case "o":
+        case 3:
           opacity = AnimatableValueParser.parseInteger(reader, composition);
           break;
-        case "lc":
+        case 4:
           capType = ShapeStroke.LineCapType.values()[reader.nextInt() - 1];
           break;
-        case "lj":
+        case 5:
           joinType = ShapeStroke.LineJoinType.values()[reader.nextInt() - 1];
           break;
-        case "ml":
-          miterLimit =  (float) reader.nextDouble();
+        case 6:
+          miterLimit = (float) reader.nextDouble();
           break;
-        case "hd":
+        case 7:
           hidden = reader.nextBoolean();
           break;
-        case "d":
+        case 8:
           reader.beginArray();
           while (reader.hasNext()) {
             String n = null;
@@ -64,14 +81,15 @@
 
             reader.beginObject();
             while (reader.hasNext()) {
-              switch (reader.nextName()) {
-                case "n":
+              switch (reader.selectName(DASH_PATTERN_NAMES)) {
+                case 0:
                   n = reader.nextString();
                   break;
-                case "v":
+                case 1:
                   val = AnimatableValueParser.parseFloat(reader, composition);
                   break;
                 default:
+                  reader.skipName();
                   reader.skipValue();
               }
             }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeTrimPathParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeTrimPathParser.java
index 20df2a3..f8e70dd 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/ShapeTrimPathParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ShapeTrimPathParser.java
@@ -1,17 +1,26 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
 
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.model.animatable.AnimatableFloatValue;
 import com.airbnb.lottie.model.content.ShapeTrimPath;
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
+import static com.airbnb.lottie.parser.moshi.JsonReader.*;
+
 class ShapeTrimPathParser {
 
   private ShapeTrimPathParser() {}
-
+  private static Options NAMES = Options.of(
+      "s",
+      "e",
+      "o",
+      "nm",
+      "m",
+      "hd"
+  );
   static ShapeTrimPath parse(
       JsonReader reader, LottieComposition composition) throws IOException {
     String name = null;
@@ -22,23 +31,23 @@
     boolean hidden = false;
 
     while (reader.hasNext()) {
-      switch (reader.nextName()) {
-        case "s":
+      switch (reader.selectName(NAMES)) {
+        case 0:
           start = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
-        case "e":
+        case 1:
           end = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
-        case "o":
+        case 2:
           offset = AnimatableValueParser.parseFloat(reader, composition, false);
           break;
-        case "nm":
+        case 3:
           name = reader.nextString();
           break;
-        case "m":
+        case 4:
           type = ShapeTrimPath.Type.forId(reader.nextInt());
           break;
-        case "hd":
+        case 5:
           hidden = reader.nextBoolean();
           break;
         default:
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ValueParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ValueParser.java
index d1685b4..3ada0e6 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/ValueParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ValueParser.java
@@ -1,6 +1,7 @@
 package com.airbnb.lottie.parser;
 
-import android.util.JsonReader;
+
+import com.airbnb.lottie.parser.moshi.JsonReader;
 
 import java.io.IOException;
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonDataException.java b/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonDataException.java
new file mode 100644
index 0000000..f3b01c1
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonDataException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.airbnb.lottie.parser.moshi;
+
+
+import androidx.annotation.Nullable;
+
+/**
+ * Thrown when the data in a JSON document doesn't match the data expected by the caller. For
+ * example, suppose the application expects a boolean but the JSON document contains a string. When
+ * the call to {@link JsonReader#nextBoolean} is made, a {@code JsonDataException} is thrown.
+ *
+ * <p>Exceptions of this type should be fixed by either changing the application code to accept
+ * the unexpected JSON, or by changing the JSON to conform to the application's expectations.
+ *
+ * <p>This exception may also be triggered if a document's nesting exceeds 31 levels. This depth is
+ * sufficient for all practical applications, but shallow enough to avoid uglier failures like
+ * {@link StackOverflowError}.
+ */
+final class JsonDataException extends RuntimeException {
+
+  JsonDataException(@Nullable String message) {
+    super(message);
+  }
+
+}
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonEncodingException.java b/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonEncodingException.java
new file mode 100644
index 0000000..accdfaa
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonEncodingException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.airbnb.lottie.parser.moshi;
+
+import java.io.IOException;
+
+import androidx.annotation.Nullable;
+
+
+/** Thrown when the data being parsed is not encoded as valid JSON. */
+final class JsonEncodingException extends IOException {
+  JsonEncodingException(@Nullable String message) {
+    super(message);
+  }
+}
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonReader.java b/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonReader.java
new file mode 100644
index 0000000..12f4506
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonReader.java
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.airbnb.lottie.parser.moshi;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import androidx.annotation.Nullable;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.ByteString;
+
+/**
+ * Reads a JSON (<a href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>)
+ * encoded value as a stream of tokens. This stream includes both literal
+ * values (strings, numbers, booleans, and nulls) as well as the begin and
+ * end delimiters of objects and arrays. The tokens are traversed in
+ * depth-first order, the same order that they appear in the JSON document.
+ * Within JSON objects, name/value pairs are represented by a single token.
+ *
+ * <h3>Parsing JSON</h3>
+ * To create a recursive descent parser for your own JSON streams, first create
+ * an entry point method that creates a {@code JsonReader}.
+ *
+ * <p>Next, create handler methods for each structure in your JSON text. You'll
+ * need a method for each object type and for each array type.
+ * <ul>
+ *   <li>Within <strong>array handling</strong> methods, first call {@link
+ *       #beginArray} to consume the array's opening bracket. Then create a
+ *       while loop that accumulates values, terminating when {@link #hasNext}
+ *       is false. Finally, read the array's closing bracket by calling {@link
+ *       #endArray}.
+ *   <li>Within <strong>object handling</strong> methods, first call {@link
+ *       #beginObject} to consume the object's opening brace. Then create a
+ *       while loop that assigns values to local variables based on their name.
+ *       This loop should terminate when {@link #hasNext} is false. Finally,
+ *       read the object's closing brace by calling {@link #endObject}.
+ * </ul>
+ * <p>When a nested object or array is encountered, delegate to the
+ * corresponding handler method.
+ *
+ * <p>When an unknown name is encountered, strict parsers should fail with an
+ * exception. Lenient parsers should call {@link #skipValue()} to recursively
+ * skip the value's nested tokens, which may otherwise conflict.
+ *
+ * <p>If a value may be null, you should first check using {@link #peek()}.
+ * Null literals can be consumed using either {@link #nextNull()} or {@link
+ * #skipValue()}.
+ *
+ * <h3>Example</h3>
+ * Suppose we'd like to parse a stream of messages such as the following: <pre> {@code
+ * [
+ *   {
+ *     "id": 912345678901,
+ *     "text": "How do I read a JSON stream in Java?",
+ *     "geo": null,
+ *     "user": {
+ *       "name": "json_newb",
+ *       "followers_count": 41
+ *      }
+ *   },
+ *   {
+ *     "id": 912345678902,
+ *     "text": "@json_newb just use JsonReader!",
+ *     "geo": [50.454722, -104.606667],
+ *     "user": {
+ *       "name": "jesse",
+ *       "followers_count": 2
+ *     }
+ *   }
+ * ]}</pre>
+ * This code implements the parser for the above structure: <pre>   {@code
+ *
+ *   public List<Message> readJsonStream(BufferedSource source) throws IOException {
+ *     JsonReader reader = JsonReader.of(source);
+ *     try {
+ *       return readMessagesArray(reader);
+ *     } finally {
+ *       reader.close();
+ *     }
+ *   }
+ *
+ *   public List<Message> readMessagesArray(JsonReader reader) throws IOException {
+ *     List<Message> messages = new ArrayList<Message>();
+ *
+ *     reader.beginArray();
+ *     while (reader.hasNext()) {
+ *       messages.add(readMessage(reader));
+ *     }
+ *     reader.endArray();
+ *     return messages;
+ *   }
+ *
+ *   public Message readMessage(JsonReader reader) throws IOException {
+ *     long id = -1;
+ *     String text = null;
+ *     User user = null;
+ *     List<Double> geo = null;
+ *
+ *     reader.beginObject();
+ *     while (reader.hasNext()) {
+ *       String name = reader.nextName();
+ *       if (name.equals("id")) {
+ *         id = reader.nextLong();
+ *       } else if (name.equals("text")) {
+ *         text = reader.nextString();
+ *       } else if (name.equals("geo") && reader.peek() != Token.NULL) {
+ *         geo = readDoublesArray(reader);
+ *       } else if (name.equals("user")) {
+ *         user = readUser(reader);
+ *       } else {
+ *         reader.skipValue();
+ *       }
+ *     }
+ *     reader.endObject();
+ *     return new Message(id, text, user, geo);
+ *   }
+ *
+ *   public List<Double> readDoublesArray(JsonReader reader) throws IOException {
+ *     List<Double> doubles = new ArrayList<Double>();
+ *
+ *     reader.beginArray();
+ *     while (reader.hasNext()) {
+ *       doubles.add(reader.nextDouble());
+ *     }
+ *     reader.endArray();
+ *     return doubles;
+ *   }
+ *
+ *   public User readUser(JsonReader reader) throws IOException {
+ *     String username = null;
+ *     int followersCount = -1;
+ *
+ *     reader.beginObject();
+ *     while (reader.hasNext()) {
+ *       String name = reader.nextName();
+ *       if (name.equals("name")) {
+ *         username = reader.nextString();
+ *       } else if (name.equals("followers_count")) {
+ *         followersCount = reader.nextInt();
+ *       } else {
+ *         reader.skipValue();
+ *       }
+ *     }
+ *     reader.endObject();
+ *     return new User(username, followersCount);
+ *   }}</pre>
+ *
+ * <h3>Number Handling</h3>
+ * This reader permits numeric values to be read as strings and string values to
+ * be read as numbers. For example, both elements of the JSON array {@code
+ * [1, "1"]} may be read using either {@link #nextInt} or {@link #nextString}.
+ * This behavior is intended to prevent lossy numeric conversions: double is
+ * JavaScript's only numeric type and very large values like {@code
+ * 9007199254740993} cannot be represented exactly on that platform. To minimize
+ * precision loss, extremely large values should be written and read as strings
+ * in JSON.
+ *
+ * <p>Each {@code JsonReader} may be used to read a single JSON stream. Instances
+ * of this class are not thread safe.
+ */
+public abstract class JsonReader implements Closeable {
+  /*
+   * From RFC 7159, "All Unicode characters may be placed within the
+   * quotation marks except for the characters that must be escaped:
+   * quotation mark, reverse solidus, and the control characters
+   * (U+0000 through U+001F)."
+   *
+   * We also escape '\u2028' and '\u2029', which JavaScript interprets as
+   * newline characters. This prevents eval() from failing with a syntax
+   * error. http://code.google.com/p/google-gson/issues/detail?id=341
+   */
+  private static final String[] REPLACEMENT_CHARS;
+  static {
+    REPLACEMENT_CHARS = new String[128];
+    for (int i = 0; i <= 0x1f; i++) {
+      REPLACEMENT_CHARS[i] = String.format("\\u%04x", (int) i);
+    }
+    REPLACEMENT_CHARS['"'] = "\\\"";
+    REPLACEMENT_CHARS['\\'] = "\\\\";
+    REPLACEMENT_CHARS['\t'] = "\\t";
+    REPLACEMENT_CHARS['\b'] = "\\b";
+    REPLACEMENT_CHARS['\n'] = "\\n";
+    REPLACEMENT_CHARS['\r'] = "\\r";
+    REPLACEMENT_CHARS['\f'] = "\\f";
+  }
+
+  // The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
+  // grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
+  // prone to trigger StackOverflowErrors.
+  int stackSize;
+  int[] scopes;
+  String[] pathNames;
+  int[] pathIndices;
+
+  /** True to accept non-spec compliant JSON. */
+  boolean lenient;
+
+  /** True to throw a {@link JsonDataException} on any attempt to call {@link #skipValue()}. */
+  boolean failOnUnknown;
+
+  /** Returns a new instance that reads UTF-8 encoded JSON from {@code source}. */
+   public static JsonReader of(BufferedSource source) {
+    return new JsonUtf8Reader(source);
+  }
+
+  // Package-private to control subclasses.
+  JsonReader() {
+    scopes = new int[32];
+    pathNames = new String[32];
+    pathIndices = new int[32];
+  }
+
+  final void pushScope(int newTop) {
+    if (stackSize == scopes.length) {
+      if (stackSize == 256) {
+        throw new JsonDataException("Nesting too deep at " + getPath());
+      }
+      scopes = Arrays.copyOf(scopes, scopes.length * 2);
+      pathNames = Arrays.copyOf(pathNames, pathNames.length * 2);
+      pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2);
+    }
+    scopes[stackSize++] = newTop;
+  }
+
+  /**
+   * Throws a new IO exception with the given message and a context snippet
+   * with this reader's content.
+   */
+  final JsonEncodingException syntaxError(String message) throws JsonEncodingException {
+    throw new JsonEncodingException(message + " at path " + getPath());
+  }
+
+
+
+
+
+  /**
+   * Consumes the next token from the JSON stream and asserts that it is the beginning of a new
+   * array.
+   */
+  public abstract void beginArray() throws IOException;
+
+  /**
+   * Consumes the next token from the JSON stream and asserts that it is the
+   * end of the current array.
+   */
+  public abstract void endArray() throws IOException;
+
+  /**
+   * Consumes the next token from the JSON stream and asserts that it is the beginning of a new
+   * object.
+   */
+  public abstract void beginObject() throws IOException;
+
+  /**
+   * Consumes the next token from the JSON stream and asserts that it is the end of the current
+   * object.
+   */
+  public abstract void endObject() throws IOException;
+
+  /**
+   * Returns true if the current array or object has another element.
+   */
+   public abstract boolean hasNext() throws IOException;
+
+  /**
+   * Returns the type of the next token without consuming it.
+   */
+   public abstract Token peek() throws IOException;
+
+  /**
+   * Returns the next token, a {@linkplain Token#NAME property name}, and consumes it.
+   *
+   * @throws JsonDataException if the next token in the stream is not a property name.
+   */
+   public abstract String nextName() throws IOException;
+
+  /**
+   * If the next token is a {@linkplain Token#NAME property name} that's in {@code options}, this
+   * consumes it and returns its index. Otherwise this returns -1 and no name is consumed.
+   */
+   public abstract int selectName(Options options) throws IOException;
+
+  /**
+   * Skips the next token, consuming it. This method is intended for use when the JSON token stream
+   * contains unrecognized or unhandled names.
+   *
+   * <p>This throws a {@link JsonDataException} if this parser has been configured to {@linkplain
+   * #failOnUnknown fail on unknown} names.
+   */
+  public abstract void skipName() throws IOException;
+
+  /**
+   * Returns the {@linkplain Token#STRING string} value of the next token, consuming it. If the next
+   * token is a number, this method will return its string form.
+   *
+   * @throws JsonDataException if the next token is not a string or if this reader is closed.
+   */
+  public abstract String nextString() throws IOException;
+
+  /**
+   * Returns the {@linkplain Token#BOOLEAN boolean} value of the next token, consuming it.
+   *
+   * @throws JsonDataException if the next token is not a boolean or if this reader is closed.
+   */
+  public abstract boolean nextBoolean() throws IOException;
+
+  /**
+   * Returns the {@linkplain Token#NUMBER double} value of the next token, consuming it. If the next
+   * token is a string, this method will attempt to parse it as a double using {@link
+   * Double#parseDouble(String)}.
+   *
+   * @throws JsonDataException if the next token is not a literal value, or if the next literal
+   *     value cannot be parsed as a double, or is non-finite.
+   */
+  public abstract double nextDouble() throws IOException;
+
+  /**
+   * Returns the {@linkplain Token#NUMBER int} value of the next token, consuming it. If the next
+   * token is a string, this method will attempt to parse it as an int. If the next token's numeric
+   * value cannot be exactly represented by a Java {@code int}, this method throws.
+   *
+   * @throws JsonDataException if the next token is not a literal value, if the next literal value
+   *     cannot be parsed as a number, or exactly represented as an int.
+   */
+  public abstract int nextInt() throws IOException;
+
+  /**
+   * Skips the next value recursively. If it is an object or array, all nested elements are skipped.
+   * This method is intended for use when the JSON token stream contains unrecognized or unhandled
+   * values.
+   *
+   * <p>This throws a {@link JsonDataException} if this parser has been configured to {@linkplain
+   * #failOnUnknown fail on unknown} values.
+   */
+  public abstract void skipValue() throws IOException;
+
+
+  /**
+   * Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
+   * the current location in the JSON value.
+   */
+   public final String getPath() {
+    return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
+  }
+
+  /**
+   * A set of strings to be chosen with {@link #selectName} or {@link #selectString}. This prepares
+   * the encoded values of the strings so they can be read directly from the input source.
+   */
+  public static final class Options {
+    final String[] strings;
+    final okio.Options doubleQuoteSuffix;
+
+    private Options(String[] strings, okio.Options doubleQuoteSuffix) {
+      this.strings = strings;
+      this.doubleQuoteSuffix = doubleQuoteSuffix;
+    }
+
+     public static Options of(String... strings) {
+      try {
+        ByteString[] result = new ByteString[strings.length];
+        Buffer buffer = new Buffer();
+        for (int i = 0; i < strings.length; i++) {
+          string(buffer, strings[i]);
+          buffer.readByte(); // Skip the leading double quote (but leave the trailing one).
+          result[i] = buffer.readByteString();
+        }
+        return new Options(strings.clone(), okio.Options.of(result));
+      } catch (IOException e) {
+        throw new AssertionError(e);
+      }
+    }
+  }
+
+  /**
+   * Writes {@code value} as a string literal to {@code sink}. This wraps the value in double quotes
+   * and escapes those characters that require it.
+   */
+  private static void string(BufferedSink sink, String value) throws IOException {
+    String[] replacements = REPLACEMENT_CHARS;
+    sink.writeByte('"');
+    int last = 0;
+    int length = value.length();
+    for (int i = 0; i < length; i++) {
+      char c = value.charAt(i);
+      String replacement;
+      if (c < 128) {
+        replacement = replacements[c];
+        if (replacement == null) {
+          continue;
+        }
+      } else if (c == '\u2028') {
+        replacement = "\\u2028";
+      } else if (c == '\u2029') {
+        replacement = "\\u2029";
+      } else {
+        continue;
+      }
+      if (last < i) {
+        sink.writeUtf8(value, last, i);
+      }
+      sink.writeUtf8(replacement);
+      last = i + 1;
+    }
+    if (last < length) {
+      sink.writeUtf8(value, last, length);
+    }
+    sink.writeByte('"');
+  }
+
+  /**
+   * A structure, name, or value type in a JSON-encoded string.
+   */
+  public enum Token {
+
+    /**
+     * The opening of a JSON array.
+     * and read using {@link JsonReader#beginArray}.
+     */
+    BEGIN_ARRAY,
+
+    /**
+     * The closing of a JSON array.
+     * and read using {@link JsonReader#endArray}.
+     */
+    END_ARRAY,
+
+    /**
+     * The opening of a JSON object.
+     * and read using {@link JsonReader#beginObject}.
+     */
+    BEGIN_OBJECT,
+
+    /**
+     * The closing of a JSON object.
+     * and read using {@link JsonReader#endObject}.
+     */
+    END_OBJECT,
+
+    /**
+     * A JSON property name. Within objects, tokens alternate between names and
+     * their values.
+     */
+    NAME,
+
+    /**
+     * A JSON string.
+     */
+    STRING,
+
+    /**
+     * A JSON number represented in this API by a Java {@code double}, {@code
+     * long}, or {@code int}.
+     */
+    NUMBER,
+
+    /**
+     * A JSON {@code true} or {@code false}.
+     */
+    BOOLEAN,
+
+    /**
+     * A JSON {@code null}.
+     */
+    NULL,
+
+    /**
+     * The end of the JSON stream. This sentinel value is returned by {@link
+     * JsonReader#peek()} to signal that the JSON-encoded value has no more
+     * tokens.
+     */
+    END_DOCUMENT
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonScope.java b/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonScope.java
new file mode 100644
index 0000000..0234af2
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonScope.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.airbnb.lottie.parser.moshi;
+
+/** Lexical scoping elements within a JSON reader or writer. */
+final class JsonScope {
+  private JsonScope() {
+  }
+
+  /** An array with no elements requires no separators or newlines before it is closed. */
+  static final int EMPTY_ARRAY = 1;
+
+  /** A array with at least one value requires a comma and newline before the next element. */
+  static final int NONEMPTY_ARRAY = 2;
+
+  /** An object with no name/value pairs requires no separators or newlines before it is closed. */
+  static final int EMPTY_OBJECT = 3;
+
+  /** An object whose most recent element is a key. The next element must be a value. */
+  static final int DANGLING_NAME = 4;
+
+  /** An object with at least one name/value pair requires a separator before the next element. */
+  static final int NONEMPTY_OBJECT = 5;
+
+  /** No object or array has been started. */
+  static final int EMPTY_DOCUMENT = 6;
+
+  /** A document with at an array or object. */
+  static final int NONEMPTY_DOCUMENT = 7;
+
+  /** A document that's been closed and cannot be accessed. */
+  static final int CLOSED = 8;
+
+  /**
+   * Renders the path in a JSON document to a string. The {@code pathNames} and {@code pathIndices}
+   * parameters corresponds directly to stack: At indices where the stack contains an object
+   * (EMPTY_OBJECT, DANGLING_NAME or NONEMPTY_OBJECT), pathNames contains the name at this scope.
+   * Where it contains an array (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index
+   * in that array. Otherwise the value is undefined, and we take advantage of that by incrementing
+   * pathIndices when doing so isn't useful.
+   */
+  static String getPath(int stackSize, int[] stack, String[] pathNames, int[] pathIndices) {
+    StringBuilder result = new StringBuilder().append('$');
+    for (int i = 0; i < stackSize; i++) {
+      switch (stack[i]) {
+        case EMPTY_ARRAY:
+        case NONEMPTY_ARRAY:
+          result.append('[').append(pathIndices[i]).append(']');
+          break;
+
+        case EMPTY_OBJECT:
+        case DANGLING_NAME:
+        case NONEMPTY_OBJECT:
+          result.append('.');
+          if (pathNames[i] != null) {
+            result.append(pathNames[i]);
+          }
+          break;
+
+        case NONEMPTY_DOCUMENT:
+        case EMPTY_DOCUMENT:
+        case CLOSED:
+          break;
+      }
+    }
+    return result.toString();
+  }
+}
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonUtf8Reader.java b/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonUtf8Reader.java
new file mode 100644
index 0000000..e39da1b
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/moshi/JsonUtf8Reader.java
@@ -0,0 +1,1044 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.airbnb.lottie.parser.moshi;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+import androidx.annotation.Nullable;
+import okio.Buffer;
+import okio.BufferedSource;
+import okio.ByteString;
+
+final class JsonUtf8Reader extends JsonReader {
+  private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10;
+
+  private static final ByteString SINGLE_QUOTE_OR_SLASH = ByteString.encodeUtf8("'\\");
+  private static final ByteString DOUBLE_QUOTE_OR_SLASH = ByteString.encodeUtf8("\"\\");
+  private static final ByteString UNQUOTED_STRING_TERMINALS
+      = ByteString.encodeUtf8("{}[]:, \n\t\r\f/\\;#=");
+  private static final ByteString LINEFEED_OR_CARRIAGE_RETURN = ByteString.encodeUtf8("\n\r");
+  private static final ByteString CLOSING_BLOCK_COMMENT = ByteString.encodeUtf8("*/");
+
+  private static final int PEEKED_NONE = 0;
+  private static final int PEEKED_BEGIN_OBJECT = 1;
+  private static final int PEEKED_END_OBJECT = 2;
+  private static final int PEEKED_BEGIN_ARRAY = 3;
+  private static final int PEEKED_END_ARRAY = 4;
+  private static final int PEEKED_TRUE = 5;
+  private static final int PEEKED_FALSE = 6;
+  private static final int PEEKED_NULL = 7;
+  private static final int PEEKED_SINGLE_QUOTED = 8;
+  private static final int PEEKED_DOUBLE_QUOTED = 9;
+  private static final int PEEKED_UNQUOTED = 10;
+  /** When this is returned, the string value is stored in peekedString. */
+  private static final int PEEKED_BUFFERED = 11;
+  private static final int PEEKED_SINGLE_QUOTED_NAME = 12;
+  private static final int PEEKED_DOUBLE_QUOTED_NAME = 13;
+  private static final int PEEKED_UNQUOTED_NAME = 14;
+  private static final int PEEKED_BUFFERED_NAME = 15;
+  /** When this is returned, the integer value is stored in peekedLong. */
+  private static final int PEEKED_LONG = 16;
+  private static final int PEEKED_NUMBER = 17;
+  private static final int PEEKED_EOF = 18;
+
+  /* State machine when parsing numbers */
+  private static final int NUMBER_CHAR_NONE = 0;
+  private static final int NUMBER_CHAR_SIGN = 1;
+  private static final int NUMBER_CHAR_DIGIT = 2;
+  private static final int NUMBER_CHAR_DECIMAL = 3;
+  private static final int NUMBER_CHAR_FRACTION_DIGIT = 4;
+  private static final int NUMBER_CHAR_EXP_E = 5;
+  private static final int NUMBER_CHAR_EXP_SIGN = 6;
+  private static final int NUMBER_CHAR_EXP_DIGIT = 7;
+
+  /** The input JSON. */
+  private final BufferedSource source;
+  private final Buffer buffer;
+
+  private int peeked = PEEKED_NONE;
+
+  /**
+   * A peeked value that was composed entirely of digits with an optional
+   * leading dash. Positive values may not have a leading 0.
+   */
+  private long peekedLong;
+
+  /**
+   * The number of characters in a peeked number literal.
+   */
+  private int peekedNumberLength;
+
+  /**
+   * A peeked string that should be parsed on the next double, long or string.
+   * This is populated before a numeric value is parsed and used if that parsing
+   * fails.
+   */
+  private @Nullable
+  String peekedString;
+
+  JsonUtf8Reader(BufferedSource source) {
+    if (source == null) {
+      throw new NullPointerException("source == null");
+    }
+    this.source = source;
+    this.buffer = source.getBuffer();
+    pushScope(JsonScope.EMPTY_DOCUMENT);
+  }
+
+
+  @Override public void beginArray() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    if (p == PEEKED_BEGIN_ARRAY) {
+      pushScope(JsonScope.EMPTY_ARRAY);
+      pathIndices[stackSize - 1] = 0;
+      peeked = PEEKED_NONE;
+    } else {
+      throw new JsonDataException("Expected BEGIN_ARRAY but was " + peek()
+          + " at path " + getPath());
+    }
+  }
+
+  @Override public void endArray() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    if (p == PEEKED_END_ARRAY) {
+      stackSize--;
+      pathIndices[stackSize - 1]++;
+      peeked = PEEKED_NONE;
+    } else {
+      throw new JsonDataException("Expected END_ARRAY but was " + peek()
+          + " at path " + getPath());
+    }
+  }
+
+  @Override public void beginObject() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    if (p == PEEKED_BEGIN_OBJECT) {
+      pushScope(JsonScope.EMPTY_OBJECT);
+      peeked = PEEKED_NONE;
+    } else {
+      throw new JsonDataException("Expected BEGIN_OBJECT but was " + peek()
+          + " at path " + getPath());
+    }
+  }
+
+  @Override public void endObject() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    if (p == PEEKED_END_OBJECT) {
+      stackSize--;
+      pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
+      pathIndices[stackSize - 1]++;
+      peeked = PEEKED_NONE;
+    } else {
+      throw new JsonDataException("Expected END_OBJECT but was " + peek()
+          + " at path " + getPath());
+    }
+  }
+
+  @Override public boolean hasNext() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY && p != PEEKED_EOF;
+  }
+
+  @Override public Token peek() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+
+    switch (p) {
+      case PEEKED_BEGIN_OBJECT:
+        return Token.BEGIN_OBJECT;
+      case PEEKED_END_OBJECT:
+        return Token.END_OBJECT;
+      case PEEKED_BEGIN_ARRAY:
+        return Token.BEGIN_ARRAY;
+      case PEEKED_END_ARRAY:
+        return Token.END_ARRAY;
+      case PEEKED_SINGLE_QUOTED_NAME:
+      case PEEKED_DOUBLE_QUOTED_NAME:
+      case PEEKED_UNQUOTED_NAME:
+      case PEEKED_BUFFERED_NAME:
+        return Token.NAME;
+      case PEEKED_TRUE:
+      case PEEKED_FALSE:
+        return Token.BOOLEAN;
+      case PEEKED_NULL:
+        return Token.NULL;
+      case PEEKED_SINGLE_QUOTED:
+      case PEEKED_DOUBLE_QUOTED:
+      case PEEKED_UNQUOTED:
+      case PEEKED_BUFFERED:
+        return Token.STRING;
+      case PEEKED_LONG:
+      case PEEKED_NUMBER:
+        return Token.NUMBER;
+      case PEEKED_EOF:
+        return Token.END_DOCUMENT;
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  private int doPeek() throws IOException {
+    int peekStack = scopes[stackSize - 1];
+    if (peekStack == JsonScope.EMPTY_ARRAY) {
+      scopes[stackSize - 1] = JsonScope.NONEMPTY_ARRAY;
+    } else if (peekStack == JsonScope.NONEMPTY_ARRAY) {
+      // Look for a comma before the next element.
+      int c = nextNonWhitespace(true);
+      buffer.readByte(); // consume ']' or ','.
+      switch (c) {
+        case ']':
+          return peeked = PEEKED_END_ARRAY;
+        case ';':
+          checkLenient(); // fall-through
+        case ',':
+          break;
+        default:
+          throw syntaxError("Unterminated array");
+      }
+    } else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) {
+      scopes[stackSize - 1] = JsonScope.DANGLING_NAME;
+      // Look for a comma before the next element.
+      if (peekStack == JsonScope.NONEMPTY_OBJECT) {
+        int c = nextNonWhitespace(true);
+        buffer.readByte(); // Consume '}' or ','.
+        switch (c) {
+          case '}':
+            return peeked = PEEKED_END_OBJECT;
+          case ';':
+            checkLenient(); // fall-through
+          case ',':
+            break;
+          default:
+            throw syntaxError("Unterminated object");
+        }
+      }
+      int c = nextNonWhitespace(true);
+      switch (c) {
+        case '"':
+          buffer.readByte(); // consume the '\"'.
+          return peeked = PEEKED_DOUBLE_QUOTED_NAME;
+        case '\'':
+          buffer.readByte(); // consume the '\''.
+          checkLenient();
+          return peeked = PEEKED_SINGLE_QUOTED_NAME;
+        case '}':
+          if (peekStack != JsonScope.NONEMPTY_OBJECT) {
+            buffer.readByte(); // consume the '}'.
+            return peeked = PEEKED_END_OBJECT;
+          } else {
+            throw syntaxError("Expected name");
+          }
+        default:
+          checkLenient();
+          if (isLiteral((char) c)) {
+            return peeked = PEEKED_UNQUOTED_NAME;
+          } else {
+            throw syntaxError("Expected name");
+          }
+      }
+    } else if (peekStack == JsonScope.DANGLING_NAME) {
+      scopes[stackSize - 1] = JsonScope.NONEMPTY_OBJECT;
+      // Look for a colon before the value.
+      int c = nextNonWhitespace(true);
+      buffer.readByte(); // Consume ':'.
+      switch (c) {
+        case ':':
+          break;
+        case '=':
+          checkLenient();
+          if (source.request(1) && buffer.getByte(0) == '>') {
+            buffer.readByte(); // Consume '>'.
+          }
+          break;
+        default:
+          throw syntaxError("Expected ':'");
+      }
+    } else if (peekStack == JsonScope.EMPTY_DOCUMENT) {
+      scopes[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT;
+    } else if (peekStack == JsonScope.NONEMPTY_DOCUMENT) {
+      int c = nextNonWhitespace(false);
+      if (c == -1) {
+        return peeked = PEEKED_EOF;
+      } else {
+        checkLenient();
+      }
+    } else if (peekStack == JsonScope.CLOSED) {
+      throw new IllegalStateException("JsonReader is closed");
+    }
+
+    int c = nextNonWhitespace(true);
+    switch (c) {
+      case ']':
+        if (peekStack == JsonScope.EMPTY_ARRAY) {
+          buffer.readByte(); // Consume ']'.
+          return peeked = PEEKED_END_ARRAY;
+        }
+        // fall-through to handle ",]"
+      case ';':
+      case ',':
+        // In lenient mode, a 0-length literal in an array means 'null'.
+        if (peekStack == JsonScope.EMPTY_ARRAY || peekStack == JsonScope.NONEMPTY_ARRAY) {
+          checkLenient();
+          return peeked = PEEKED_NULL;
+        } else {
+          throw syntaxError("Unexpected value");
+        }
+      case '\'':
+        checkLenient();
+        buffer.readByte(); // Consume '\''.
+        return peeked = PEEKED_SINGLE_QUOTED;
+      case '"':
+        buffer.readByte(); // Consume '\"'.
+        return peeked = PEEKED_DOUBLE_QUOTED;
+      case '[':
+        buffer.readByte(); // Consume '['.
+        return peeked = PEEKED_BEGIN_ARRAY;
+      case '{':
+        buffer.readByte(); // Consume '{'.
+        return peeked = PEEKED_BEGIN_OBJECT;
+      default:
+    }
+
+    int result = peekKeyword();
+    if (result != PEEKED_NONE) {
+      return result;
+    }
+
+    result = peekNumber();
+    if (result != PEEKED_NONE) {
+      return result;
+    }
+
+    if (!isLiteral(buffer.getByte(0))) {
+      throw syntaxError("Expected value");
+    }
+
+    checkLenient();
+    return peeked = PEEKED_UNQUOTED;
+  }
+
+  private int peekKeyword() throws IOException {
+    // Figure out which keyword we're matching against by its first character.
+    byte c = buffer.getByte(0);
+    String keyword;
+    String keywordUpper;
+    int peeking;
+    if (c == 't' || c == 'T') {
+      keyword = "true";
+      keywordUpper = "TRUE";
+      peeking = PEEKED_TRUE;
+    } else if (c == 'f' || c == 'F') {
+      keyword = "false";
+      keywordUpper = "FALSE";
+      peeking = PEEKED_FALSE;
+    } else if (c == 'n' || c == 'N') {
+      keyword = "null";
+      keywordUpper = "NULL";
+      peeking = PEEKED_NULL;
+    } else {
+      return PEEKED_NONE;
+    }
+
+    // Confirm that chars [1..length) match the keyword.
+    int length = keyword.length();
+    for (int i = 1; i < length; i++) {
+      if (!source.request(i + 1)) {
+        return PEEKED_NONE;
+      }
+      c = buffer.getByte(i);
+      if (c != keyword.charAt(i) && c != keywordUpper.charAt(i)) {
+        return PEEKED_NONE;
+      }
+    }
+
+    if (source.request(length + 1) && isLiteral(buffer.getByte(length))) {
+      return PEEKED_NONE; // Don't match trues, falsey or nullsoft!
+    }
+
+    // We've found the keyword followed either by EOF or by a non-literal character.
+    buffer.skip(length);
+    return peeked = peeking;
+  }
+
+  private int peekNumber() throws IOException {
+    long value = 0; // Negative to accommodate Long.MIN_VALUE more easily.
+    boolean negative = false;
+    boolean fitsInLong = true;
+    int last = NUMBER_CHAR_NONE;
+
+    int i = 0;
+
+    charactersOfNumber:
+    for (; true; i++) {
+      if (!source.request(i + 1)) {
+        break;
+      }
+
+      byte c = buffer.getByte(i);
+      switch (c) {
+        case '-':
+          if (last == NUMBER_CHAR_NONE) {
+            negative = true;
+            last = NUMBER_CHAR_SIGN;
+            continue;
+          } else if (last == NUMBER_CHAR_EXP_E) {
+            last = NUMBER_CHAR_EXP_SIGN;
+            continue;
+          }
+          return PEEKED_NONE;
+
+        case '+':
+          if (last == NUMBER_CHAR_EXP_E) {
+            last = NUMBER_CHAR_EXP_SIGN;
+            continue;
+          }
+          return PEEKED_NONE;
+
+        case 'e':
+        case 'E':
+          if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT) {
+            last = NUMBER_CHAR_EXP_E;
+            continue;
+          }
+          return PEEKED_NONE;
+
+        case '.':
+          if (last == NUMBER_CHAR_DIGIT) {
+            last = NUMBER_CHAR_DECIMAL;
+            continue;
+          }
+          return PEEKED_NONE;
+
+        default:
+          if (c < '0' || c > '9') {
+            if (!isLiteral(c)) {
+              break charactersOfNumber;
+            }
+            return PEEKED_NONE;
+          }
+          if (last == NUMBER_CHAR_SIGN || last == NUMBER_CHAR_NONE) {
+            value = -(c - '0');
+            last = NUMBER_CHAR_DIGIT;
+          } else if (last == NUMBER_CHAR_DIGIT) {
+            if (value == 0) {
+              return PEEKED_NONE; // Leading '0' prefix is not allowed (since it could be octal).
+            }
+            long newValue = value * 10 - (c - '0');
+            fitsInLong &= value > MIN_INCOMPLETE_INTEGER
+                || (value == MIN_INCOMPLETE_INTEGER && newValue < value);
+            value = newValue;
+          } else if (last == NUMBER_CHAR_DECIMAL) {
+            last = NUMBER_CHAR_FRACTION_DIGIT;
+          } else if (last == NUMBER_CHAR_EXP_E || last == NUMBER_CHAR_EXP_SIGN) {
+            last = NUMBER_CHAR_EXP_DIGIT;
+          }
+      }
+    }
+
+    // We've read a complete number. Decide if it's a PEEKED_LONG or a PEEKED_NUMBER.
+    if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative)
+        && (value != 0 || !negative)) {
+      peekedLong = negative ? value : -value;
+      buffer.skip(i);
+      return peeked = PEEKED_LONG;
+    } else if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT
+        || last == NUMBER_CHAR_EXP_DIGIT) {
+      peekedNumberLength = i;
+      return peeked = PEEKED_NUMBER;
+    } else {
+      return PEEKED_NONE;
+    }
+  }
+
+  private boolean isLiteral(int c) throws IOException {
+    switch (c) {
+      case '/':
+      case '\\':
+      case ';':
+      case '#':
+      case '=':
+        checkLenient(); // fall-through
+      case '{':
+      case '}':
+      case '[':
+      case ']':
+      case ':':
+      case ',':
+      case ' ':
+      case '\t':
+      case '\f':
+      case '\r':
+      case '\n':
+        return false;
+      default:
+        return true;
+    }
+  }
+
+  @Override public String nextName() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    String result;
+    if (p == PEEKED_UNQUOTED_NAME) {
+      result = nextUnquotedValue();
+    } else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
+      result = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH);
+    } else if (p == PEEKED_SINGLE_QUOTED_NAME) {
+      result = nextQuotedValue(SINGLE_QUOTE_OR_SLASH);
+    } else if (p == PEEKED_BUFFERED_NAME) {
+      result = peekedString;
+    } else {
+      throw new JsonDataException("Expected a name but was " + peek() + " at path " + getPath());
+    }
+    peeked = PEEKED_NONE;
+    pathNames[stackSize - 1] = result;
+    return result;
+  }
+
+  @Override public int selectName(Options options) throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    if (p < PEEKED_SINGLE_QUOTED_NAME || p > PEEKED_BUFFERED_NAME) {
+      return -1;
+    }
+    if (p == PEEKED_BUFFERED_NAME) {
+      return findName(peekedString, options);
+    }
+
+    int result = source.select(options.doubleQuoteSuffix);
+    if (result != -1) {
+      peeked = PEEKED_NONE;
+      pathNames[stackSize - 1] = options.strings[result];
+
+      return result;
+    }
+
+    // The next name may be unnecessary escaped. Save the last recorded path name, so that we
+    // can restore the peek state in case we fail to find a match.
+    String lastPathName = pathNames[stackSize - 1];
+
+    String nextName = nextName();
+    result = findName(nextName, options);
+
+    if (result == -1) {
+      peeked = PEEKED_BUFFERED_NAME;
+      peekedString = nextName;
+      // We can't push the path further, make it seem like nothing happened.
+      pathNames[stackSize - 1] = lastPathName;
+    }
+
+    return result;
+  }
+
+  @Override public void skipName() throws IOException {
+    if (failOnUnknown) {
+      throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
+    }
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    if (p == PEEKED_UNQUOTED_NAME) {
+      skipUnquotedValue();
+    } else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
+      skipQuotedValue(DOUBLE_QUOTE_OR_SLASH);
+    } else if (p == PEEKED_SINGLE_QUOTED_NAME) {
+      skipQuotedValue(SINGLE_QUOTE_OR_SLASH);
+    } else if (p != PEEKED_BUFFERED_NAME) {
+      throw new JsonDataException("Expected a name but was " + peek() + " at path " + getPath());
+    }
+    peeked = PEEKED_NONE;
+    pathNames[stackSize - 1] = "null";
+  }
+
+  /**
+   * If {@code name} is in {@code options} this consumes it and returns its index.
+   * Otherwise this returns -1 and no name is consumed.
+   */
+  private int findName(String name, Options options) {
+    for (int i = 0, size = options.strings.length; i < size; i++) {
+      if (name.equals(options.strings[i])) {
+        peeked = PEEKED_NONE;
+        pathNames[stackSize - 1] = name;
+
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  @Override public String nextString() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    String result;
+    if (p == PEEKED_UNQUOTED) {
+      result = nextUnquotedValue();
+    } else if (p == PEEKED_DOUBLE_QUOTED) {
+      result = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH);
+    } else if (p == PEEKED_SINGLE_QUOTED) {
+      result = nextQuotedValue(SINGLE_QUOTE_OR_SLASH);
+    } else if (p == PEEKED_BUFFERED) {
+      result = peekedString;
+      peekedString = null;
+    } else if (p == PEEKED_LONG) {
+      result = Long.toString(peekedLong);
+    } else if (p == PEEKED_NUMBER) {
+      result = buffer.readUtf8(peekedNumberLength);
+    } else {
+      throw new JsonDataException("Expected a string but was " + peek() + " at path " + getPath());
+    }
+    peeked = PEEKED_NONE;
+    pathIndices[stackSize - 1]++;
+    return result;
+  }
+
+  /**
+   * If {@code string} is in {@code options} this consumes it and returns its index.
+   * Otherwise this returns -1 and no string is consumed.
+   */
+  private int findString(String string, Options options) {
+    for (int i = 0, size = options.strings.length; i < size; i++) {
+      if (string.equals(options.strings[i])) {
+        peeked = PEEKED_NONE;
+        pathIndices[stackSize - 1]++;
+
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  @Override public boolean nextBoolean() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    if (p == PEEKED_TRUE) {
+      peeked = PEEKED_NONE;
+      pathIndices[stackSize - 1]++;
+      return true;
+    } else if (p == PEEKED_FALSE) {
+      peeked = PEEKED_NONE;
+      pathIndices[stackSize - 1]++;
+      return false;
+    }
+    throw new JsonDataException("Expected a boolean but was " + peek() + " at path " + getPath());
+  }
+
+  @Override public double nextDouble() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+
+    if (p == PEEKED_LONG) {
+      peeked = PEEKED_NONE;
+      pathIndices[stackSize - 1]++;
+      return (double) peekedLong;
+    }
+
+    if (p == PEEKED_NUMBER) {
+      peekedString = buffer.readUtf8(peekedNumberLength);
+    } else if (p == PEEKED_DOUBLE_QUOTED) {
+      peekedString = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH);
+    } else if (p == PEEKED_SINGLE_QUOTED) {
+      peekedString = nextQuotedValue(SINGLE_QUOTE_OR_SLASH);
+    } else if (p == PEEKED_UNQUOTED) {
+      peekedString = nextUnquotedValue();
+    } else if (p != PEEKED_BUFFERED) {
+      throw new JsonDataException("Expected a double but was " + peek() + " at path " + getPath());
+    }
+
+    peeked = PEEKED_BUFFERED;
+    double result;
+    try {
+      result = Double.parseDouble(peekedString);
+    } catch (NumberFormatException e) {
+      throw new JsonDataException("Expected a double but was " + peekedString
+          + " at path " + getPath());
+    }
+    if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) {
+      throw new JsonEncodingException("JSON forbids NaN and infinities: " + result
+          + " at path " + getPath());
+    }
+    peekedString = null;
+    peeked = PEEKED_NONE;
+    pathIndices[stackSize - 1]++;
+    return result;
+  }
+
+  /**
+   * Returns the string up to but not including {@code quote}, unescaping any character escape
+   * sequences encountered along the way. The opening quote should have already been read. This
+   * consumes the closing quote, but does not include it in the returned string.
+   *
+   * @throws IOException if any unicode escape sequences are malformed.
+   */
+  private String nextQuotedValue(ByteString runTerminator) throws IOException {
+    StringBuilder builder = null;
+    while (true) {
+      long index = source.indexOfElement(runTerminator);
+      if (index == -1L) throw syntaxError("Unterminated string");
+
+      // If we've got an escape character, we're going to need a string builder.
+      if (buffer.getByte(index) == '\\') {
+        if (builder == null) builder = new StringBuilder();
+        builder.append(buffer.readUtf8(index));
+        buffer.readByte(); // '\'
+        builder.append(readEscapeCharacter());
+        continue;
+      }
+
+      // If it isn't the escape character, it's the quote. Return the string.
+      if (builder == null) {
+        String result = buffer.readUtf8(index);
+        buffer.readByte(); // Consume the quote character.
+        return result;
+      } else {
+        builder.append(buffer.readUtf8(index));
+        buffer.readByte(); // Consume the quote character.
+        return builder.toString();
+      }
+    }
+  }
+
+  /** Returns an unquoted value as a string. */
+  private String nextUnquotedValue() throws IOException {
+    long i = source.indexOfElement(UNQUOTED_STRING_TERMINALS);
+    return i != -1 ? buffer.readUtf8(i) : buffer.readUtf8();
+  }
+
+  private void skipQuotedValue(ByteString runTerminator) throws IOException {
+    while (true) {
+      long index = source.indexOfElement(runTerminator);
+      if (index == -1L) throw syntaxError("Unterminated string");
+
+      if (buffer.getByte(index) == '\\') {
+        buffer.skip(index + 1);
+        readEscapeCharacter();
+      } else {
+        buffer.skip(index + 1);
+        return;
+      }
+    }
+  }
+
+  private void skipUnquotedValue() throws IOException {
+    long i = source.indexOfElement(UNQUOTED_STRING_TERMINALS);
+    buffer.skip(i != -1L ? i : buffer.size());
+  }
+
+  @Override public int nextInt() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+
+    int result;
+    if (p == PEEKED_LONG) {
+      result = (int) peekedLong;
+      if (peekedLong != result) { // Make sure no precision was lost casting to 'int'.
+        throw new JsonDataException("Expected an int but was " + peekedLong
+            + " at path " + getPath());
+      }
+      peeked = PEEKED_NONE;
+      pathIndices[stackSize - 1]++;
+      return result;
+    }
+
+    if (p == PEEKED_NUMBER) {
+      peekedString = buffer.readUtf8(peekedNumberLength);
+    } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_SINGLE_QUOTED) {
+      peekedString = p == PEEKED_DOUBLE_QUOTED
+          ? nextQuotedValue(DOUBLE_QUOTE_OR_SLASH)
+          : nextQuotedValue(SINGLE_QUOTE_OR_SLASH);
+      try {
+        result = Integer.parseInt(peekedString);
+        peeked = PEEKED_NONE;
+        pathIndices[stackSize - 1]++;
+        return result;
+      } catch (NumberFormatException ignored) {
+        // Fall back to parse as a double below.
+      }
+    } else if (p != PEEKED_BUFFERED) {
+      throw new JsonDataException("Expected an int but was " + peek() + " at path " + getPath());
+    }
+
+    peeked = PEEKED_BUFFERED;
+    double asDouble;
+    try {
+      asDouble = Double.parseDouble(peekedString);
+    } catch (NumberFormatException e) {
+      throw new JsonDataException("Expected an int but was " + peekedString
+          + " at path " + getPath());
+    }
+    result = (int) asDouble;
+    if (result != asDouble) { // Make sure no precision was lost casting to 'int'.
+      throw new JsonDataException("Expected an int but was " + peekedString
+          + " at path " + getPath());
+    }
+    peekedString = null;
+    peeked = PEEKED_NONE;
+    pathIndices[stackSize - 1]++;
+    return result;
+  }
+
+  @Override public void close() throws IOException {
+    peeked = PEEKED_NONE;
+    scopes[0] = JsonScope.CLOSED;
+    stackSize = 1;
+    buffer.clear();
+    source.close();
+  }
+
+  @Override public void skipValue() throws IOException {
+    if (failOnUnknown) {
+      throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
+    }
+    int count = 0;
+    do {
+      int p = peeked;
+      if (p == PEEKED_NONE) {
+        p = doPeek();
+      }
+
+      if (p == PEEKED_BEGIN_ARRAY) {
+        pushScope(JsonScope.EMPTY_ARRAY);
+        count++;
+      } else if (p == PEEKED_BEGIN_OBJECT) {
+        pushScope(JsonScope.EMPTY_OBJECT);
+        count++;
+      } else if (p == PEEKED_END_ARRAY) {
+        count--;
+        if (count < 0) {
+          throw new JsonDataException(
+              "Expected a value but was " + peek() + " at path " + getPath());
+        }
+        stackSize--;
+      } else if (p == PEEKED_END_OBJECT) {
+        count--;
+        if (count < 0) {
+          throw new JsonDataException(
+              "Expected a value but was " + peek() + " at path " + getPath());
+        }
+        stackSize--;
+      } else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) {
+        skipUnquotedValue();
+      } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) {
+        skipQuotedValue(DOUBLE_QUOTE_OR_SLASH);
+      } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) {
+        skipQuotedValue(SINGLE_QUOTE_OR_SLASH);
+      } else if (p == PEEKED_NUMBER) {
+        buffer.skip(peekedNumberLength);
+      } else if (p == PEEKED_EOF) {
+        throw new JsonDataException(
+            "Expected a value but was " + peek() + " at path " + getPath());
+      }
+      peeked = PEEKED_NONE;
+    } while (count != 0);
+
+    pathIndices[stackSize - 1]++;
+    pathNames[stackSize - 1] = "null";
+  }
+
+  /**
+   * Returns the next character in the stream that is neither whitespace nor a
+   * part of a comment. When this returns, the returned character is always at
+   * {@code buffer.getByte(0)}.
+   */
+  private int nextNonWhitespace(boolean throwOnEof) throws IOException {
+    /*
+     * This code uses ugly local variables 'p' and 'l' representing the 'pos'
+     * and 'limit' fields respectively. Using locals rather than fields saves
+     * a few field reads for each whitespace character in a pretty-printed
+     * document, resulting in a 5% speedup. We need to flush 'p' to its field
+     * before any (potentially indirect) call to fillBuffer() and reread both
+     * 'p' and 'l' after any (potentially indirect) call to the same method.
+     */
+    int p = 0;
+    while (source.request(p + 1)) {
+      int c = buffer.getByte(p++);
+      if (c == '\n' || c == ' ' || c == '\r' || c == '\t') {
+        continue;
+      }
+
+      buffer.skip(p - 1);
+      if (c == '/') {
+        if (!source.request(2)) {
+          return c;
+        }
+
+        checkLenient();
+        byte peek = buffer.getByte(1);
+        switch (peek) {
+          case '*':
+            // skip a /* c-style comment */
+            buffer.readByte(); // '/'
+            buffer.readByte(); // '*'
+            if (!skipToEndOfBlockComment()) {
+              throw syntaxError("Unterminated comment");
+            }
+            p = 0;
+            continue;
+
+          case '/':
+            // skip a // end-of-line comment
+            buffer.readByte(); // '/'
+            buffer.readByte(); // '/'
+            skipToEndOfLine();
+            p = 0;
+            continue;
+
+          default:
+            return c;
+        }
+      } else if (c == '#') {
+        // Skip a # hash end-of-line comment. The JSON RFC doesn't specify this behaviour, but it's
+        // required to parse existing documents.
+        checkLenient();
+        skipToEndOfLine();
+        p = 0;
+      } else {
+        return c;
+      }
+    }
+    if (throwOnEof) {
+      throw new EOFException("End of input");
+    } else {
+      return -1;
+    }
+  }
+
+  private void checkLenient() throws IOException {
+    if (!lenient) {
+      throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON");
+    }
+  }
+
+  /**
+   * Advances the position until after the next newline character. If the line
+   * is terminated by "\r\n", the '\n' must be consumed as whitespace by the
+   * caller.
+   */
+  private void skipToEndOfLine() throws IOException {
+    long index = source.indexOfElement(LINEFEED_OR_CARRIAGE_RETURN);
+    buffer.skip(index != -1 ? index + 1 : buffer.size());
+  }
+
+  /**
+   * Skips through the next closing block comment.
+   */
+  private boolean skipToEndOfBlockComment() throws IOException {
+    long index = source.indexOf(CLOSING_BLOCK_COMMENT);
+    boolean found = index != -1;
+    buffer.skip(found ? index + CLOSING_BLOCK_COMMENT.size() : buffer.size());
+    return found;
+  }
+
+
+  @Override public String toString() {
+    return "JsonReader(" + source + ")";
+  }
+
+  /**
+   * Unescapes the character identified by the character or characters that immediately follow a
+   * backslash. The backslash '\' should have already been read. This supports both unicode escapes
+   * "u000A" and two-character escapes "\n".
+   *
+   * @throws IOException if any unicode escape sequences are malformed.
+   */
+  private char readEscapeCharacter() throws IOException {
+    if (!source.request(1)) {
+      throw syntaxError("Unterminated escape sequence");
+    }
+
+    byte escaped = buffer.readByte();
+    switch (escaped) {
+      case 'u':
+        if (!source.request(4)) {
+          throw new EOFException("Unterminated escape sequence at path " + getPath());
+        }
+        // Equivalent to Integer.parseInt(stringPool.get(buffer, pos, 4), 16);
+        char result = 0;
+        for (int i = 0, end = i + 4; i < end; i++) {
+          byte c = buffer.getByte(i);
+          result <<= 4;
+          if (c >= '0' && c <= '9') {
+            result += (c - '0');
+          } else if (c >= 'a' && c <= 'f') {
+            result += (c - 'a' + 10);
+          } else if (c >= 'A' && c <= 'F') {
+            result += (c - 'A' + 10);
+          } else {
+            throw syntaxError("\\u" + buffer.readUtf8(4));
+          }
+        }
+        buffer.skip(4);
+        return result;
+
+      case 't':
+        return '\t';
+
+      case 'b':
+        return '\b';
+
+      case 'n':
+        return '\n';
+
+      case 'r':
+        return '\r';
+
+      case 'f':
+        return '\f';
+
+      case '\n':
+      case '\'':
+      case '"':
+      case '\\':
+      case '/':
+        return (char) escaped;
+
+      default:
+        if (!lenient) throw syntaxError("Invalid escape sequence: \\" + (char) escaped);
+        return (char) escaped;
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/moshi/LinkedHashTreeMap.java b/lottie/src/main/java/com/airbnb/lottie/parser/moshi/LinkedHashTreeMap.java
new file mode 100644
index 0000000..4537da9
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/moshi/LinkedHashTreeMap.java
@@ -0,0 +1,861 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ * Copyright (C) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.airbnb.lottie.parser.moshi;
+
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+/**
+ * A map of comparable keys to values. Unlike {@code TreeMap}, this class uses
+ * insertion order for iteration order. Comparison order is only used as an
+ * optimization for efficient insertion and removal.
+ *
+ * <p>This implementation was derived from Android 4.1's TreeMap and
+ * LinkedHashMap classes.
+ */
+final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Serializable {
+  @SuppressWarnings({ "unchecked", "rawtypes" }) // to avoid Comparable<Comparable<Comparable<...>>>
+  private static final Comparator<Comparable> NATURAL_ORDER = new Comparator<Comparable>() {
+    public int compare(Comparable a, Comparable b) {
+      return a.compareTo(b);
+    }
+  };
+
+  Comparator<? super K> comparator;
+  Node<K, V>[] table;
+  final Node<K, V> header;
+  int size = 0;
+  int modCount = 0;
+  int threshold;
+
+  /**
+   * Create a natural order, empty tree map whose keys must be mutually
+   * comparable and non-null.
+   */
+  LinkedHashTreeMap() {
+    this(null);
+  }
+
+  /**
+   * Create a tree map ordered by {@code comparator}. This map's keys may only
+   * be null if {@code comparator} permits.
+   *
+   * @param comparator the comparator to order elements with, or {@code null} to
+   *     use the natural ordering.
+   */
+  @SuppressWarnings({
+      "unchecked", "rawtypes" // Unsafe! if comparator is null, this assumes K is comparable.
+  })
+  LinkedHashTreeMap(Comparator<? super K> comparator) {
+    this.comparator = comparator != null
+        ? comparator
+        : (Comparator) NATURAL_ORDER;
+    this.header = new Node<>();
+    this.table = new Node[16]; // TODO: sizing/resizing policies
+    this.threshold = (table.length / 2) + (table.length / 4); // 3/4 capacity
+  }
+
+  @Override public int size() {
+    return size;
+  }
+
+  @Override public V get(Object key) {
+    Node<K, V> node = findByObject(key);
+    return node != null ? node.value : null;
+  }
+
+  @Override public boolean containsKey(Object key) {
+    return findByObject(key) != null;
+  }
+
+  @Override public V put(K key, V value) {
+    if (key == null) {
+      throw new NullPointerException("key == null");
+    }
+    Node<K, V> created = find(key, true);
+    V result = created.value;
+    created.value = value;
+    return result;
+  }
+
+  @Override public void clear() {
+    Arrays.fill(table, null);
+    size = 0;
+    modCount++;
+
+    // Clear all links to help GC
+    Node<K, V> header = this.header;
+    for (Node<K, V> e = header.next; e != header; ) {
+      Node<K, V> next = e.next;
+      e.next = e.prev = null;
+      e = next;
+    }
+
+    header.next = header.prev = header;
+  }
+
+  @Override public V remove(Object key) {
+    Node<K, V> node = removeInternalByKey(key);
+    return node != null ? node.value : null;
+  }
+
+  /**
+   * Returns the node at or adjacent to the given key, creating it if requested.
+   *
+   * @throws ClassCastException if {@code key} and the tree's keys aren't
+   *     mutually comparable.
+   */
+  Node<K, V> find(K key, boolean create) {
+    Comparator<? super K> comparator = this.comparator;
+    Node<K, V>[] table = this.table;
+    int hash = secondaryHash(key.hashCode());
+    int index = hash & (table.length - 1);
+    Node<K, V> nearest = table[index];
+    int comparison = 0;
+
+    if (nearest != null) {
+      // Micro-optimization: avoid polymorphic calls to Comparator.compare().
+      @SuppressWarnings("unchecked") // Throws a ClassCastException below if there's trouble.
+      Comparable<Object> comparableKey = (comparator == NATURAL_ORDER)
+          ? (Comparable<Object>) key
+          : null;
+
+      while (true) {
+        comparison = (comparableKey != null)
+            ? comparableKey.compareTo(nearest.key)
+            : comparator.compare(key, nearest.key);
+
+        // We found the requested key.
+        if (comparison == 0) {
+          return nearest;
+        }
+
+        // If it exists, the key is in a subtree. Go deeper.
+        Node<K, V> child = (comparison < 0) ? nearest.left : nearest.right;
+        if (child == null) {
+          break;
+        }
+
+        nearest = child;
+      }
+    }
+
+    // The key doesn't exist in this tree.
+    if (!create) {
+      return null;
+    }
+
+    // Create the node and add it to the tree or the table.
+    Node<K, V> header = this.header;
+    Node<K, V> created;
+    if (nearest == null) {
+      // Check that the value is comparable if we didn't do any comparisons.
+      if (comparator == NATURAL_ORDER && !(key instanceof Comparable)) {
+        throw new ClassCastException(key.getClass().getName() + " is not Comparable");
+      }
+      created = new Node<>(nearest, key, hash, header, header.prev);
+      table[index] = created;
+    } else {
+      created = new Node<>(nearest, key, hash, header, header.prev);
+      if (comparison < 0) { // nearest.key is higher
+        nearest.left = created;
+      } else { // comparison > 0, nearest.key is lower
+        nearest.right = created;
+      }
+      rebalance(nearest, true);
+    }
+
+    if (size++ > threshold) {
+      doubleCapacity();
+    }
+    modCount++;
+
+    return created;
+  }
+
+  @SuppressWarnings("unchecked")
+  Node<K, V> findByObject(Object key) {
+    try {
+      return key != null ? find((K) key, false) : null;
+    } catch (ClassCastException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Returns this map's entry that has the same key and value as {@code
+   * entry}, or null if this map has no such entry.
+   *
+   * <p>This method uses the comparator for key equality rather than {@code
+   * equals}. If this map's comparator isn't consistent with equals (such as
+   * {@code String.CASE_INSENSITIVE_ORDER}), then {@code remove()} and {@code
+   * contains()} will violate the collections API.
+   */
+  Node<K, V> findByEntry(Entry<?, ?> entry) {
+    Node<K, V> mine = findByObject(entry.getKey());
+    boolean valuesEqual = mine != null && equal(mine.value, entry.getValue());
+    return valuesEqual ? mine : null;
+  }
+
+  private boolean equal(Object a, Object b) {
+    return a == b || (a != null && a.equals(b));
+  }
+
+  /**
+   * Applies a supplemental hash function to a given hashCode, which defends
+   * against poor quality hash functions. This is critical because HashMap
+   * uses power-of-two length hash tables, that otherwise encounter collisions
+   * for hashCodes that do not differ in lower or upper bits.
+   */
+  private static int secondaryHash(int h) {
+    // Doug Lea's supplemental hash function
+    h ^= (h >>> 20) ^ (h >>> 12);
+    return h ^ (h >>> 7) ^ (h >>> 4);
+  }
+
+  /**
+   * Removes {@code node} from this tree, rearranging the tree's structure as
+   * necessary.
+   *
+   * @param unlink true to also unlink this node from the iteration linked list.
+   */
+  void removeInternal(Node<K, V> node, boolean unlink) {
+    if (unlink) {
+      node.prev.next = node.next;
+      node.next.prev = node.prev;
+      node.next = node.prev = null; // Help the GC (for performance)
+    }
+
+    Node<K, V> left = node.left;
+    Node<K, V> right = node.right;
+    Node<K, V> originalParent = node.parent;
+    if (left != null && right != null) {
+
+      /*
+       * To remove a node with both left and right subtrees, move an
+       * adjacent node from one of those subtrees into this node's place.
+       *
+       * Removing the adjacent node may change this node's subtrees. This
+       * node may no longer have two subtrees once the adjacent node is
+       * gone!
+       */
+
+      Node<K, V> adjacent = (left.height > right.height) ? left.last() : right.first();
+      removeInternal(adjacent, false); // takes care of rebalance and size--
+
+      int leftHeight = 0;
+      left = node.left;
+      if (left != null) {
+        leftHeight = left.height;
+        adjacent.left = left;
+        left.parent = adjacent;
+        node.left = null;
+      }
+      int rightHeight = 0;
+      right = node.right;
+      if (right != null) {
+        rightHeight = right.height;
+        adjacent.right = right;
+        right.parent = adjacent;
+        node.right = null;
+      }
+      adjacent.height = Math.max(leftHeight, rightHeight) + 1;
+      replaceInParent(node, adjacent);
+      return;
+    } else if (left != null) {
+      replaceInParent(node, left);
+      node.left = null;
+    } else if (right != null) {
+      replaceInParent(node, right);
+      node.right = null;
+    } else {
+      replaceInParent(node, null);
+    }
+
+    rebalance(originalParent, false);
+    size--;
+    modCount++;
+  }
+
+  Node<K, V> removeInternalByKey(Object key) {
+    Node<K, V> node = findByObject(key);
+    if (node != null) {
+      removeInternal(node, true);
+    }
+    return node;
+  }
+
+  private void replaceInParent(Node<K, V> node, Node<K, V> replacement) {
+    Node<K, V> parent = node.parent;
+    node.parent = null;
+    if (replacement != null) {
+      replacement.parent = parent;
+    }
+
+    if (parent != null) {
+      if (parent.left == node) {
+        parent.left = replacement;
+      } else {
+        assert (parent.right == node);
+        parent.right = replacement;
+      }
+    } else {
+      int index = node.hash & (table.length - 1);
+      table[index] = replacement;
+    }
+  }
+
+  /**
+   * Rebalances the tree by making any AVL rotations necessary between the
+   * newly-unbalanced node and the tree's root.
+   *
+   * @param insert true if the node was unbalanced by an insert; false if it
+   *     was by a removal.
+   */
+  private void rebalance(Node<K, V> unbalanced, boolean insert) {
+    for (Node<K, V> node = unbalanced; node != null; node = node.parent) {
+      Node<K, V> left = node.left;
+      Node<K, V> right = node.right;
+      int leftHeight = left != null ? left.height : 0;
+      int rightHeight = right != null ? right.height : 0;
+
+      int delta = leftHeight - rightHeight;
+      if (delta == -2) {
+        Node<K, V> rightLeft = right.left;
+        Node<K, V> rightRight = right.right;
+        int rightRightHeight = rightRight != null ? rightRight.height : 0;
+        int rightLeftHeight = rightLeft != null ? rightLeft.height : 0;
+
+        int rightDelta = rightLeftHeight - rightRightHeight;
+        if (rightDelta == -1 || (rightDelta == 0 && !insert)) {
+          rotateLeft(node); // AVL right right
+        } else {
+          assert (rightDelta == 1);
+          rotateRight(right); // AVL right left
+          rotateLeft(node);
+        }
+        if (insert) {
+          break; // no further rotations will be necessary
+        }
+
+      } else if (delta == 2) {
+        Node<K, V> leftLeft = left.left;
+        Node<K, V> leftRight = left.right;
+        int leftRightHeight = leftRight != null ? leftRight.height : 0;
+        int leftLeftHeight = leftLeft != null ? leftLeft.height : 0;
+
+        int leftDelta = leftLeftHeight - leftRightHeight;
+        if (leftDelta == 1 || (leftDelta == 0 && !insert)) {
+          rotateRight(node); // AVL left left
+        } else {
+          assert (leftDelta == -1);
+          rotateLeft(left); // AVL left right
+          rotateRight(node);
+        }
+        if (insert) {
+          break; // no further rotations will be necessary
+        }
+
+      } else if (delta == 0) {
+        node.height = leftHeight + 1; // leftHeight == rightHeight
+        if (insert) {
+          break; // the insert caused balance, so rebalancing is done!
+        }
+
+      } else {
+        assert (delta == -1 || delta == 1);
+        node.height = Math.max(leftHeight, rightHeight) + 1;
+        if (!insert) {
+          break; // the height hasn't changed, so rebalancing is done!
+        }
+      }
+    }
+  }
+
+  /**
+   * Rotates the subtree so that its root's right child is the new root.
+   */
+  private void rotateLeft(Node<K, V> root) {
+    Node<K, V> left = root.left;
+    Node<K, V> pivot = root.right;
+    Node<K, V> pivotLeft = pivot.left;
+    Node<K, V> pivotRight = pivot.right;
+
+    // move the pivot's left child to the root's right
+    root.right = pivotLeft;
+    if (pivotLeft != null) {
+      pivotLeft.parent = root;
+    }
+
+    replaceInParent(root, pivot);
+
+    // move the root to the pivot's left
+    pivot.left = root;
+    root.parent = pivot;
+
+    // fix heights
+    root.height = Math.max(left != null ? left.height : 0,
+        pivotLeft != null ? pivotLeft.height : 0) + 1;
+    pivot.height = Math.max(root.height,
+        pivotRight != null ? pivotRight.height : 0) + 1;
+  }
+
+  /**
+   * Rotates the subtree so that its root's left child is the new root.
+   */
+  private void rotateRight(Node<K, V> root) {
+    Node<K, V> pivot = root.left;
+    Node<K, V> right = root.right;
+    Node<K, V> pivotLeft = pivot.left;
+    Node<K, V> pivotRight = pivot.right;
+
+    // move the pivot's right child to the root's left
+    root.left = pivotRight;
+    if (pivotRight != null) {
+      pivotRight.parent = root;
+    }
+
+    replaceInParent(root, pivot);
+
+    // move the root to the pivot's right
+    pivot.right = root;
+    root.parent = pivot;
+
+    // fixup heights
+    root.height = Math.max(right != null ? right.height : 0,
+        pivotRight != null ? pivotRight.height : 0) + 1;
+    pivot.height = Math.max(root.height,
+        pivotLeft != null ? pivotLeft.height : 0) + 1;
+  }
+
+  private EntrySet entrySet;
+  private KeySet keySet;
+
+  @Override public Set<Entry<K, V>> entrySet() {
+    EntrySet result = entrySet;
+    return result != null ? result : (entrySet = new EntrySet());
+  }
+
+  @Override public Set<K> keySet() {
+    KeySet result = keySet;
+    return result != null ? result : (keySet = new KeySet());
+  }
+
+  static final class Node<K, V> implements Entry<K, V> {
+    Node<K, V> parent;
+    Node<K, V> left;
+    Node<K, V> right;
+    Node<K, V> next;
+    Node<K, V> prev;
+    final K key;
+    final int hash;
+    V value;
+    int height;
+
+    /** Create the header entry. */
+    Node() {
+      key = null;
+      hash = -1;
+      next = prev = this;
+    }
+
+    /** Create a regular entry. */
+    Node(Node<K, V> parent, K key, int hash, Node<K, V> next, Node<K, V> prev) {
+      this.parent = parent;
+      this.key = key;
+      this.hash = hash;
+      this.height = 1;
+      this.next = next;
+      this.prev = prev;
+      prev.next = this;
+      next.prev = this;
+    }
+
+    public K getKey() {
+      return key;
+    }
+
+    public V getValue() {
+      return value;
+    }
+
+    public V setValue(V value) {
+      V oldValue = this.value;
+      this.value = value;
+      return oldValue;
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Override public boolean equals(Object o) {
+      if (o instanceof Entry) {
+        Entry other = (Entry) o;
+        return (key == null ? other.getKey() == null : key.equals(other.getKey()))
+            && (value == null ? other.getValue() == null : value.equals(other.getValue()));
+      }
+      return false;
+    }
+
+    @Override public int hashCode() {
+      return (key == null ? 0 : key.hashCode())
+          ^ (value == null ? 0 : value.hashCode());
+    }
+
+    @Override public String toString() {
+      return key + "=" + value;
+    }
+
+    /**
+     * Returns the first node in this subtree.
+     */
+    public Node<K, V> first() {
+      Node<K, V> node = this;
+      Node<K, V> child = node.left;
+      while (child != null) {
+        node = child;
+        child = node.left;
+      }
+      return node;
+    }
+
+    /**
+     * Returns the last node in this subtree.
+     */
+    public Node<K, V> last() {
+      Node<K, V> node = this;
+      Node<K, V> child = node.right;
+      while (child != null) {
+        node = child;
+        child = node.right;
+      }
+      return node;
+    }
+  }
+
+  private void doubleCapacity() {
+    table = doubleCapacity(table);
+    threshold = (table.length / 2) + (table.length / 4); // 3/4 capacity
+  }
+
+  /**
+   * Returns a new array containing the same nodes as {@code oldTable}, but with
+   * twice as many trees, each of (approximately) half the previous size.
+   */
+  static <K, V> Node<K, V>[] doubleCapacity(Node<K, V>[] oldTable) {
+    // TODO: don't do anything if we're already at MAX_CAPACITY
+    int oldCapacity = oldTable.length;
+    @SuppressWarnings("unchecked") // Arrays and generics don't get along.
+        Node<K, V>[] newTable = new Node[oldCapacity * 2];
+    AvlIterator<K, V> iterator = new AvlIterator<>();
+    AvlBuilder<K, V> leftBuilder = new AvlBuilder<>();
+    AvlBuilder<K, V> rightBuilder = new AvlBuilder<>();
+
+    // Split each tree into two trees.
+    for (int i = 0; i < oldCapacity; i++) {
+      Node<K, V> root = oldTable[i];
+      if (root == null) {
+        continue;
+      }
+
+      // Compute the sizes of the left and right trees.
+      iterator.reset(root);
+      int leftSize = 0;
+      int rightSize = 0;
+      for (Node<K, V> node; (node = iterator.next()) != null; ) {
+        if ((node.hash & oldCapacity) == 0) {
+          leftSize++;
+        } else {
+          rightSize++;
+        }
+      }
+
+      // Split the tree into two.
+      leftBuilder.reset(leftSize);
+      rightBuilder.reset(rightSize);
+      iterator.reset(root);
+      for (Node<K, V> node; (node = iterator.next()) != null; ) {
+        if ((node.hash & oldCapacity) == 0) {
+          leftBuilder.add(node);
+        } else {
+          rightBuilder.add(node);
+        }
+      }
+
+      // Populate the enlarged array with these new roots.
+      newTable[i] = leftSize > 0 ? leftBuilder.root() : null;
+      newTable[i + oldCapacity] = rightSize > 0 ? rightBuilder.root() : null;
+    }
+    return newTable;
+  }
+
+  /**
+   * Walks an AVL tree in iteration order. Once a node has been returned, its
+   * left, right and parent links are <strong>no longer used</strong>. For this
+   * reason it is safe to transform these links as you walk a tree.
+   *
+   * <p><strong>Warning:</strong> this iterator is destructive. It clears the
+   * parent node of all nodes in the tree. It is an error to make a partial
+   * iteration of a tree.
+   */
+  static class AvlIterator<K, V> {
+    /** This stack is a singly linked list, linked by the 'parent' field. */
+    private Node<K, V> stackTop;
+
+    void reset(Node<K, V> root) {
+      Node<K, V> stackTop = null;
+      for (Node<K, V> n = root; n != null; n = n.left) {
+        n.parent = stackTop;
+        stackTop = n; // Stack push.
+      }
+      this.stackTop = stackTop;
+    }
+
+    public Node<K, V> next() {
+      Node<K, V> stackTop = this.stackTop;
+      if (stackTop == null) {
+        return null;
+      }
+      Node<K, V> result = stackTop;
+      stackTop = result.parent;
+      result.parent = null;
+      for (Node<K, V> n = result.right; n != null; n = n.left) {
+        n.parent = stackTop;
+        stackTop = n; // Stack push.
+      }
+      this.stackTop = stackTop;
+      return result;
+    }
+  }
+
+  /**
+   * Builds AVL trees of a predetermined size by accepting nodes of increasing
+   * value. To use:
+   * <ol>
+   *   <li>Call {@link #reset} to initialize the target size <i>size</i>.
+   *   <li>Call {@link #add} <i>size</i> times with increasing values.
+   *   <li>Call {@link #root} to get the root of the balanced tree.
+   * </ol>
+   *
+   * <p>The returned tree will satisfy the AVL constraint: for every node
+   * <i>N</i>, the height of <i>N.left</i> and <i>N.right</i> is different by at
+   * most 1. It accomplishes this by omitting deepest-level leaf nodes when
+   * building trees whose size isn't a power of 2 minus 1.
+   *
+   * <p>Unlike rebuilding a tree from scratch, this approach requires no value
+   * comparisons. Using this class to create a tree of size <i>S</i> is
+   * {@code O(S)}.
+   */
+  static final class AvlBuilder<K, V> {
+    /** This stack is a singly linked list, linked by the 'parent' field. */
+    private Node<K, V> stack;
+    private int leavesToSkip;
+    private int leavesSkipped;
+    private int size;
+
+    void reset(int targetSize) {
+      // compute the target tree size. This is a power of 2 minus one, like 15 or 31.
+      int treeCapacity = Integer.highestOneBit(targetSize) * 2 - 1;
+      leavesToSkip = treeCapacity - targetSize;
+      size = 0;
+      leavesSkipped = 0;
+      stack = null;
+    }
+
+    void add(Node<K, V> node) {
+      node.left = node.parent = node.right = null;
+      node.height = 1;
+
+      // Skip a leaf if necessary.
+      if (leavesToSkip > 0 && (size & 1) == 0) {
+        size++;
+        leavesToSkip--;
+        leavesSkipped++;
+      }
+
+      node.parent = stack;
+      stack = node; // Stack push.
+      size++;
+
+      // Skip a leaf if necessary.
+      if (leavesToSkip > 0 && (size & 1) == 0) {
+        size++;
+        leavesToSkip--;
+        leavesSkipped++;
+      }
+
+      /*
+       * Combine 3 nodes into subtrees whenever the size is one less than a
+       * multiple of 4. For example we combine the nodes A, B, C into a
+       * 3-element tree with B as the root.
+       *
+       * Combine two subtrees and a spare single value whenever the size is one
+       * less than a multiple of 8. For example at 8 we may combine subtrees
+       * (A B C) and (E F G) with D as the root to form ((A B C) D (E F G)).
+       *
+       * Just as we combine single nodes when size nears a multiple of 4, and
+       * 3-element trees when size nears a multiple of 8, we combine subtrees of
+       * size (N-1) whenever the total size is 2N-1 whenever N is a power of 2.
+       */
+      for (int scale = 4; (size & scale - 1) == scale - 1; scale *= 2) {
+        if (leavesSkipped == 0) {
+          // Pop right, center and left, then make center the top of the stack.
+          Node<K, V> right = stack;
+          Node<K, V> center = right.parent;
+          Node<K, V> left = center.parent;
+          center.parent = left.parent;
+          stack = center;
+          // Construct a tree.
+          center.left = left;
+          center.right = right;
+          center.height = right.height + 1;
+          left.parent = center;
+          right.parent = center;
+        } else if (leavesSkipped == 1) {
+          // Pop right and center, then make center the top of the stack.
+          Node<K, V> right = stack;
+          Node<K, V> center = right.parent;
+          stack = center;
+          // Construct a tree with no left child.
+          center.right = right;
+          center.height = right.height + 1;
+          right.parent = center;
+          leavesSkipped = 0;
+        } else if (leavesSkipped == 2) {
+          leavesSkipped = 0;
+        }
+      }
+    }
+
+    Node<K, V> root() {
+      Node<K, V> stackTop = this.stack;
+      if (stackTop.parent != null) {
+        throw new IllegalStateException();
+      }
+      return stackTop;
+    }
+  }
+
+  abstract class LinkedTreeMapIterator<T> implements Iterator<T> {
+    Node<K, V> next = header.next;
+    Node<K, V> lastReturned = null;
+    int expectedModCount = modCount;
+
+    public final boolean hasNext() {
+      return next != header;
+    }
+
+    final Node<K, V> nextNode() {
+      Node<K, V> e = next;
+      if (e == header) {
+        throw new NoSuchElementException();
+      }
+      if (modCount != expectedModCount) {
+        throw new ConcurrentModificationException();
+      }
+      next = e.next;
+      return lastReturned = e;
+    }
+
+    public final void remove() {
+      if (lastReturned == null) {
+        throw new IllegalStateException();
+      }
+      removeInternal(lastReturned, true);
+      lastReturned = null;
+      expectedModCount = modCount;
+    }
+  }
+
+  final class EntrySet extends AbstractSet<Entry<K, V>> {
+    @Override public int size() {
+      return size;
+    }
+
+    @Override public Iterator<Entry<K, V>> iterator() {
+      return new LinkedTreeMapIterator<Entry<K, V>>() {
+        public Entry<K, V> next() {
+          return nextNode();
+        }
+      };
+    }
+
+    @Override public boolean contains(Object o) {
+      return o instanceof Entry && findByEntry((Entry<?, ?>) o) != null;
+    }
+
+    @Override public boolean remove(Object o) {
+      if (!(o instanceof Entry)) {
+        return false;
+      }
+
+      Node<K, V> node = findByEntry((Entry<?, ?>) o);
+      if (node == null) {
+        return false;
+      }
+      removeInternal(node, true);
+      return true;
+    }
+
+    @Override public void clear() {
+      LinkedHashTreeMap.this.clear();
+    }
+  }
+
+  final class KeySet extends AbstractSet<K> {
+    @Override public int size() {
+      return size;
+    }
+
+    @Override public Iterator<K> iterator() {
+      return new LinkedTreeMapIterator<K>() {
+        public K next() {
+          return nextNode().key;
+        }
+      };
+    }
+
+    @Override public boolean contains(Object o) {
+      return containsKey(o);
+    }
+
+    @Override public boolean remove(Object key) {
+      return removeInternalByKey(key) != null;
+    }
+
+    @Override public void clear() {
+      LinkedHashTreeMap.this.clear();
+    }
+  }
+
+  /**
+   * If somebody is unlucky enough to have to serialize one of these, serialize
+   * it as a LinkedHashMap so that they won't need Gson on the other side to
+   * deserialize it. Using serialization defeats our DoS defence, so most apps
+   * shouldn't use it.
+   */
+  private Object writeReplace() throws ObjectStreamException {
+    return new LinkedHashMap<>(this);
+  }
+}
\ No newline at end of file
diff --git a/lottie/src/test/java/com/airbnb/lottie/LottieCompositionFactoryTest.java b/lottie/src/test/java/com/airbnb/lottie/LottieCompositionFactoryTest.java
index 70a6b4f..7693858 100644
--- a/lottie/src/test/java/com/airbnb/lottie/LottieCompositionFactoryTest.java
+++ b/lottie/src/test/java/com/airbnb/lottie/LottieCompositionFactoryTest.java
@@ -1,9 +1,9 @@
 package com.airbnb.lottie;
 
-import android.util.JsonReader;
-
 import com.airbnb.lottie.model.LottieCompositionCache;
 
+import com.airbnb.lottie.parser.moshi.JsonReader;
+import org.apache.tools.ant.filters.StringInputStream;
 import org.junit.Before;
 import org.junit.Test;
 import org.robolectric.RuntimeEnvironment;
@@ -11,10 +11,13 @@
 import java.io.FileNotFoundException;
 import java.io.StringReader;
 
+import static com.airbnb.lottie.parser.moshi.JsonReader.of;
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertNull;
+import static okio.Okio.buffer;
+import static okio.Okio.source;
 import static org.junit.Assert.assertTrue;
 
 public class LottieCompositionFactoryTest extends BaseTest {
@@ -50,7 +53,7 @@
 
     @Test
     public void testLoadJsonReader() {
-        JsonReader reader = new JsonReader(new StringReader(JSON));
+        JsonReader reader = JsonReader.of(buffer(source(new StringInputStream(JSON))));
         LottieResult<LottieComposition> result = LottieCompositionFactory.fromJsonReaderSync(reader, "json");
         assertNull(result.getException());
         assertNotNull(result.getValue());
@@ -58,7 +61,7 @@
 
     @Test
     public void testLoadInvalidJsonReader() {
-        JsonReader reader = new JsonReader(new StringReader(NOT_JSON));
+        JsonReader reader = JsonReader.of(buffer(source(new StringInputStream(NOT_JSON))));
         LottieResult<LottieComposition> result = LottieCompositionFactory.fromJsonReaderSync(reader, "json");
         assertNotNull(result.getException());
         assertNull(result.getValue());
@@ -87,7 +90,7 @@
 
     @Test
     public void testNullMultipleTimesAsync() {
-        JsonReader reader = new JsonReader(new StringReader(JSON));
+        JsonReader reader = JsonReader.of(buffer(source(new StringInputStream(JSON))));
         LottieTask<LottieComposition> task1 = LottieCompositionFactory.fromJsonReader(reader, null);
         LottieTask<LottieComposition> task2 = LottieCompositionFactory.fromJsonReader(reader, null);
         assertFalse(task1 == task2);
@@ -95,7 +98,7 @@
 
     @Test
     public void testNullMultipleTimesSync() {
-        JsonReader reader = new JsonReader(new StringReader(JSON));
+        JsonReader reader = JsonReader.of(buffer(source(new StringInputStream(JSON))));
         LottieResult<LottieComposition> task1 = LottieCompositionFactory.fromJsonReaderSync(reader, null);
         LottieResult<LottieComposition> task2 = LottieCompositionFactory.fromJsonReaderSync(reader, null);
         assertFalse(task1 == task2);
@@ -103,7 +106,7 @@
 
     @Test
     public void testCacheWorks() {
-        JsonReader reader = new JsonReader(new StringReader(JSON));
+        JsonReader reader = JsonReader.of(buffer(source(new StringInputStream(JSON))));
         LottieTask<LottieComposition> task1 = LottieCompositionFactory.fromJsonReader(reader, "foo");
         LottieTask<LottieComposition> task2 = LottieCompositionFactory.fromJsonReader(reader, "foo");
         assertTrue(task1 == task2);
@@ -111,7 +114,7 @@
 
     @Test
     public void testZeroCacheWorks() {
-        JsonReader reader = new JsonReader(new StringReader(JSON));
+        JsonReader reader = JsonReader.of(buffer(source(new StringInputStream(JSON))));
         LottieCompositionFactory.setMaxCacheSize(1);
         LottieResult<LottieComposition> taskFoo1 = LottieCompositionFactory.fromJsonReaderSync(reader, "foo");
         LottieResult<LottieComposition> taskBar = LottieCompositionFactory.fromJsonReaderSync(reader, "bar");