Merge branch 'main' into feature/UIMA-6266-Clean-JSON-Wire-Format-for-CAS

* main:
  [UIMA-6413] Memory leak in FSClassRegistry
  [UIMA-6413] Memory leak in FSClassRegistry
  [UIMA-6413] Memory leak in FSClassRegistry
  [UIMA-6373] Format UIMA Core Java SDK codebase
diff --git a/uimaj-core/pom.xml b/uimaj-core/pom.xml
index 845ef0a..877734a 100644
--- a/uimaj-core/pom.xml
+++ b/uimaj-core/pom.xml
@@ -242,8 +242,19 @@
     <finalName>uima-core</finalName>
     <pluginManagement>
       <plugins>
-      
-        <!-- Uncomment the next to run with -Xlint:unchecked  -->
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-jar-plugin</artifactId>
+          <executions>
+            <execution>
+              <goals>
+                <goal>test-jar</goal>
+              </goals>
+            </execution>
+          </executions>
+        </plugin>
+
+        <!-- Uncomment the next to run with -Xlint:unchecked -->
         <!--plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasSerializationDeserialization_XCAS_Test.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasSerializationDeserialization_XCAS_Test.java
index faa2bc4..7a62724 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasSerializationDeserialization_XCAS_Test.java
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasSerializationDeserialization_XCAS_Test.java
@@ -20,7 +20,7 @@
 
 import static java.util.Arrays.asList;
 import static org.apache.uima.cas.SerialFormat.XCAS;
-import static org.apache.uima.cas.serdes.SerDesAssuptions.assumeNotKnownToFail;
+import static org.apache.uima.cas.serdes.SerDesAssumptions.assumeNotKnownToFail;
 import static org.apache.uima.cas.serdes.SerDesCasIOTestUtils.createCasMaybeWithTypesystem;
 import static org.apache.uima.cas.serdes.SerDesCasIOTestUtils.desser;
 import static org.apache.uima.cas.serdes.SerDesCasIOTestUtils.serdes;
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasToComparableText.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasToComparableText.java
index 007a721..3a79a98 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasToComparableText.java
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasToComparableText.java
@@ -367,7 +367,9 @@
       sectionHeader.add("<COVERED_TEXT>");
     }
 
-    listFeatures(aType).stream().filter(f -> !isExcluded(f)).map(f -> f.getShortName())
+    listFeatures(aType).stream() //
+            .filter(f -> !isExcluded(f)) //
+            .map(f -> f.getShortName()) //
             .forEachOrdered(sectionHeader::add);
     aCSV.printRecord(sectionHeader);
   }
@@ -906,7 +908,12 @@
       }
 
       // Annotation? Then sort by offsets
-      if (aFS1 instanceof AnnotationFS && aFS2 instanceof AnnotationFS) {
+      boolean fs1IsAnnotation = aFS1 instanceof AnnotationFS;
+      boolean fs2IsAnnotation = aFS2 instanceof AnnotationFS;
+      if (fs1IsAnnotation != fs2IsAnnotation) {
+        return -1;
+      }
+      if (fs1IsAnnotation && fs2IsAnnotation) {
         AnnotationFS ann1 = (AnnotationFS) aFS1;
         AnnotationFS ann2 = (AnnotationFS) aFS2;
 
@@ -917,7 +924,7 @@
         }
 
         // Descending by end
-        int endCmp = ann1.getEnd() - ann2.getEnd();
+        int endCmp = ann2.getEnd() - ann1.getEnd();
         if (endCmp != 0) {
           return endCmp;
         }
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/PerformanceTestRunner.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/PerformanceTestRunner.java
new file mode 100644
index 0000000..536bfbf
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/PerformanceTestRunner.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.cas.serdes;
+
+import static java.lang.System.currentTimeMillis;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import javax.annotation.Generated;
+
+import org.apache.commons.lang3.function.FailableBiConsumer;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.serdes.generators.CasGenerator;
+import org.apache.uima.resource.ResourceInitializationException;
+import org.apache.uima.resource.metadata.TypeSystemDescription;
+import org.apache.uima.util.CasCreationUtils;
+
+public class PerformanceTestRunner {
+  private final FailableBiConsumer<InputStream, CAS, Exception> deserializer;
+  private final FailableBiConsumer<CAS, OutputStream, Exception> serializer;
+  private final int iterations;
+  private final int warmUpIterations;
+
+  private CasGenerator generator;
+  private CAS randomizedCas;
+  private CAS targetCas;
+  private byte[] randomizedCasBytes;
+
+  private PerformanceTestRunner(Builder builder) {
+    this.iterations = builder.iterations;
+    this.warmUpIterations = builder.warmUpIterations;
+    this.generator = builder.generator;
+    this.serializer = builder.serializer;
+    this.deserializer = builder.deserializer;
+
+    try {
+      TypeSystemDescription tsd = generator.generateTypeSystem();
+      randomizedCas = generator.generateCas(tsd);
+      targetCas = CasCreationUtils.createCas(tsd, null, null, null);
+    } catch (ResourceInitializationException e) {
+      throw new IllegalStateException(e);
+    }
+
+    try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+      serializer.accept(randomizedCas, bos);
+      randomizedCasBytes = bos.toByteArray();
+    } catch (Exception e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  public void writeData(Path aPath) throws IOException {
+    Files.createDirectories(aPath.getParent());
+    try (OutputStream os = Files.newOutputStream(aPath)) {
+      os.write(randomizedCasBytes);
+    }
+  }
+
+  public int getDataSize() {
+    return randomizedCasBytes.length;
+  }
+
+  public long measureSerializationPerformance() throws Exception {
+    long total = 0;
+    for (int i = 0; i < iterations + warmUpIterations; i++) {
+      try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+        long start = currentTimeMillis();
+        serializer.accept(randomizedCas, bos);
+        if (i >= warmUpIterations) {
+          total += currentTimeMillis() - start;
+        }
+      }
+    }
+
+    return total;
+  }
+
+  public long measureDeserializationPerformance() throws Exception {
+    long total = 0;
+    for (int i = 0; i < iterations + warmUpIterations; i++) {
+      try (ByteArrayInputStream bis = new ByteArrayInputStream(randomizedCasBytes)) {
+        long start = currentTimeMillis();
+        deserializer.accept(bis, targetCas);
+        if (i >= warmUpIterations) {
+          total += currentTimeMillis() - start;
+        }
+      }
+
+      targetCas.reset();
+    }
+
+    return total;
+  }
+
+  /**
+   * Creates builder to build {@link PerformanceTestRunner}.
+   * 
+   * @return created builder
+   */
+  @Generated("SparkTools")
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /**
+   * Builder to build {@link PerformanceTestRunner}.
+   */
+  public static final class Builder {
+    private int iterations = 100;
+    private int warmUpIterations = 5;
+    private CasGenerator generator;
+    private FailableBiConsumer<InputStream, CAS, Exception> deserializer;
+    private FailableBiConsumer<CAS, OutputStream, Exception> serializer;
+
+    private Builder() {
+    }
+
+    public Builder withSerializer(FailableBiConsumer<CAS, OutputStream, Exception> aSerializer) {
+      this.serializer = aSerializer;
+      return this;
+    }
+
+    public Builder withDeserializer(FailableBiConsumer<InputStream, CAS, Exception> aDeserializer) {
+      this.deserializer = aDeserializer;
+      return this;
+    }
+
+    public Builder withIterations(int iterations) {
+      this.iterations = iterations;
+      return this;
+    }
+
+    public Builder withWarmUpIterations(int iterations) {
+      this.warmUpIterations = iterations;
+      return this;
+    }
+
+    public Builder withGenerator(CasGenerator generator) {
+      this.generator = generator;
+      return this;
+    }
+
+    public PerformanceTestRunner build() {
+      return new PerformanceTestRunner(this);
+    }
+  }
+
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/Performance_SerialFormat_Test.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/Performance_SerialFormat_Test.java
new file mode 100644
index 0000000..fa3b717
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/Performance_SerialFormat_Test.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.cas.serdes;
+
+import static org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE;
+
+import java.util.Random;
+
+import org.apache.uima.cas.SerialFormat;
+import org.apache.uima.cas.serdes.generators.MultiFeatureRandomCasGenerator;
+import org.apache.uima.util.CasIOUtils;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+public class Performance_SerialFormat_Test {
+
+  private static final int ITERATIONS = 100;
+  private static final int SIZE = 1_000;
+
+  @ParameterizedTest
+  @EnumSource(value = SerialFormat.class, mode = EXCLUDE, names = { "UNKNOWN",
+      "COMPRESSED_PROJECTION" })
+  public void jsonSerialization(SerialFormat aFormat) throws Exception {
+    PerformanceTestRunner runner = buildRunner(aFormat);
+    long serDuration = runner.measureSerializationPerformance();
+
+    System.out.printf("[%23s] %d CASes with %d feature structures (%7d bytes each)%n", aFormat,
+            ITERATIONS, SIZE, runner.getDataSize());
+
+    System.out.printf("[%23s]   %6s ms serialization    %6.2f fs/sec  %6.2f CAS/sec %n", aFormat,
+            serDuration, (ITERATIONS * SIZE) / (serDuration / 1000.0d),
+            ITERATIONS / (serDuration / 1000.0d));
+
+    long desDuration = runner.measureDeserializationPerformance();
+
+    System.out.printf("[%23s]   %6s ms deserialization  %6.2f fs/sec  %6.2f CAS/sec %n", aFormat,
+            desDuration, (ITERATIONS * SIZE) / (desDuration / 1000.0d),
+            ITERATIONS / (desDuration / 1000.0d));
+  }
+
+  public PerformanceTestRunner buildRunner(SerialFormat aFormat) throws Exception {
+    return PerformanceTestRunner.builder() //
+            .withIterations(ITERATIONS) //
+            .withDeserializer(CasIOUtils::load)
+            .withSerializer((cas, os) -> CasIOUtils.save(cas, os, aFormat)) //
+            .withGenerator(MultiFeatureRandomCasGenerator.builder() //
+                    .withRandomGenerator(new Random(123456l)) //
+                    .withSize(SIZE) //
+                    .build()) //
+            .build();
+  }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssumptions.java
similarity index 97%
rename from uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
rename to uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssumptions.java
index 6ba69f5..6f25a52 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssumptions.java
@@ -22,7 +22,7 @@
 
 import java.util.regex.Pattern;
 
-public class SerDesAssuptions {
+public class SerDesAssumptions {
   public static void assumeNotKnownToFail(Runnable aScenario, String... aPatternsAndReasons) {
     for (int i = 0; i < aPatternsAndReasons.length; i += 2) {
       String pattern = aPatternsAndReasons[i];
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/generators/MultiFeatureRandomCasGenerator.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/generators/MultiFeatureRandomCasGenerator.java
index c171bb3..5e47095 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/generators/MultiFeatureRandomCasGenerator.java
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/generators/MultiFeatureRandomCasGenerator.java
@@ -86,9 +86,11 @@
   private static final short[] SHORT_VALUES = { 1, 0, -1, Short.MAX_VALUE, Short.MIN_VALUE, 22,
       -22 };
   private static final double[] DOUBLE_VALUES = { 1d, 0d, -1d, Double.MAX_VALUE,
-      /* Double.MIN_NORMAL, */ Double.MIN_VALUE, 33d, -33.33d };
+      /* Double.MIN_NORMAL, */ Double.MIN_VALUE, 33d, -33.33d, Double.NaN, Double.NEGATIVE_INFINITY,
+      Double.POSITIVE_INFINITY };
   private static final float[] FLOAT_VALUES = { 1f, 0f, -1f, Float.MAX_VALUE,
-      /* Float.MIN_NORMAL, */ Float.MIN_VALUE, 17f, -22.33f };
+      /* Float.MIN_NORMAL, */ Float.MIN_VALUE, 17f, -22.33f, Float.NaN, Float.NEGATIVE_INFINITY,
+      Float.POSITIVE_INFINITY };
 
   /**
    * set to true to change FS creation to keep references to all created FS.
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerDesTestScenario.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerDesTestScenario.java
index 02bb063..bf0b951 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerDesTestScenario.java
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerDesTestScenario.java
@@ -18,6 +18,7 @@
  */
 package org.apache.uima.cas.serdes.scenario;
 
+import static org.apache.uima.cas.impl.CasCompare.compareCASes;
 import static org.apache.uima.cas.serdes.CasToComparableText.toComparableString;
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -25,7 +26,6 @@
 import org.apache.commons.lang3.function.FailableSupplier;
 import org.apache.uima.cas.CAS;
 import org.apache.uima.cas.impl.CASImpl;
-import org.apache.uima.cas.impl.CasCompare;
 import org.apache.uima.cas.serdes.transitions.CasSerDesCycleConfiguration;
 import org.apache.uima.cas.serdes.transitions.CasSourceTargetConfiguration;
 import org.assertj.core.internal.Failures;
@@ -91,7 +91,7 @@
 
     assertThat(targetCas) //
             .as("CasCompare comparison must match (%s)", debugInfo)
-            .usingComparator((a, b) -> CasCompare.compareCASes((CASImpl) a, (CASImpl) b) ? 0 : 1) //
+            .usingComparator((a, b) -> compareCASes((CASImpl) a, (CASImpl) b) ? 0 : 1) //
             .isEqualTo(sourceCas);
   }
 
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerRefTestScenario.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerRefTestScenario.java
index fc6d15b..9d99a9c 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerRefTestScenario.java
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerRefTestScenario.java
@@ -22,6 +22,7 @@
 import static org.apache.uima.cas.serdes.SerDesCasIOTestUtils.writeXmi;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.contentOf;
+import static org.assertj.core.api.Assumptions.assumeThat;
 
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -98,7 +99,7 @@
     writeTypeSystemDescription(sourceCas, targetCasFile.resolveSibling("debug-typesystem.xml"));
 
     // Compare the serialized CAS file against the reference
-    assertThat(referenceCasFile.toFile()) //
+    assumeThat(referenceCasFile.toFile()) //
             .as("Reference file must exists at %s", referenceCasFile) //
             .exists();
     assertThat(contentOf(targetCasFile.toFile())) //
diff --git a/uimaj-json/pom.xml b/uimaj-json/pom.xml
index eef3abd..7bb20a3 100644
--- a/uimaj-json/pom.xml
+++ b/uimaj-json/pom.xml
@@ -17,7 +17,9 @@
    specific language governing permissions and limitations
    under the License.
 -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
     <groupId>org.apache.uima</groupId>
@@ -28,70 +30,142 @@
   <artifactId>uimaj-json</artifactId>
   <name>Apache UIMA Base: ${project.artifactId}: JSON</name>
   <description>JSON support for UIMA SDK</description>
-  
+
   <scm>
     <connection>scm:git:https://github.com/apache/uima-uimaj/</connection>
     <developerConnection>scm:git:https://github.com/apache/uima-uimaj/</developerConnection>
     <url>https://github.com/apache/uima-uimaj/</url>
     <tag>uimaj-3.2.0</tag>
   </scm>
-  
+
   <properties>
     <uimaScmProject>${project.artifactId}</uimaScmProject>
   </properties>
-  
+
   <dependencies>
     <dependency>
       <groupId>org.apache.uima</groupId>
       <artifactId>uimaj-core</artifactId>
       <version>${project.parent.version}</version>
     </dependency>
-  
+
     <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
       <artifactId>jackson-core</artifactId>
       <version>${jackson.version}</version>
     </dependency>
-    
+
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>${jackson.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-annotations</artifactId>
+      <version>${jackson.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.uima</groupId>
       <artifactId>uimaj-test-util</artifactId>
       <version>${project.parent.version}</version>
       <scope>test</scope>
     </dependency>
-
+    <dependency>
+      <groupId>org.apache.uima</groupId>
+      <artifactId>uimaj-core</artifactId>
+      <version>${project.parent.version}</version>
+      <classifier>tests</classifier>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-jdk14</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.skyscreamer</groupId>
+      <artifactId>jsonassert</artifactId>
+      <version>1.5.0</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>net.javacrumbs.json-unit</groupId>
+      <artifactId>json-unit-assertj</artifactId>
+      <version>2.28.0</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>net.javacrumbs.json-unit</groupId>
+      <artifactId>json-unit-json-path</artifactId>
+      <version>2.28.0</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+      <version>3.12.0</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+      <version>2.7</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-csv</artifactId>
+      <version>1.8</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.uima</groupId>
+      <artifactId>uimaj-core</artifactId>
+      <version>${project.parent.version}</version>
+      <classifier>tests</classifier>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
-  
+
   <build>
     <pluginManagement>
-	    <plugins>
-	      <plugin>
-	          <groupId>org.apache.rat</groupId>
-	          <artifactId>apache-rat-plugin</artifactId>
-	          <executions>
-	            <execution>
-	              <id>default-cli</id>
-	              <configuration>
-	                <excludes combine.children="append">
-	                  <exclude>src/test/resources/CasSerialization/expected/json/*.txt</exclude>
-                    <exclude>src/test/resources/CasSerialization/expected/xmi/*.xml</exclude>
-	                  <exclude>src/test/resources/CASTests/json/expected/*.json</exclude>
-	                  <exclude>src/test/java/org/apache/uima/test/*.java</exclude>
-	               </excludes>
-	              </configuration>
-	            </execution>
-	          </executions>
-	      </plugin>
-	    </plugins>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.rat</groupId>
+          <artifactId>apache-rat-plugin</artifactId>
+          <executions>
+            <execution>
+              <id>default-cli</id>
+              <configuration>
+                <excludes combine.children="append">
+                  <exclude>src/test/resources/**/data.json</exclude>
+                  <exclude>src/test/resources/**/debug.xmi</exclude>
+                  <exclude>src/test/resources/**/debug-typesystem.xml</exclude>
+                  <exclude>src/test/resources/FlexJsonDeserializer/**/*.json</exclude>
+                  <exclude>src/test/resources/FlexJsonSerializerTest/**/*.json</exclude>
+                  <exclude>src/test/resources/CasSerialization/expected/xmi/*.xml</exclude>
+                  <exclude>src/test/resources/CasSerialization/expected/json/*.txt</exclude>
+                  <exclude>src/test/resources/CASTests/json/expected/*.json</exclude>
+                  <exclude>src/test/java/org/apache/uima/test/*.java</exclude>
+                </excludes>
+              </configuration>
+            </execution>
+          </executions>
+        </plugin>
+      </plugins>
     </pluginManagement>
-    
-    <plugins>
-    </plugins>
   </build>
-  
 </project>
\ No newline at end of file
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/flexjson/FlexJsonCasDeserializer.java b/uimaj-json/src/main/java/org/apache/uima/json/flexjson/FlexJsonCasDeserializer.java
new file mode 100644
index 0000000..f0d5e56
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/flexjson/FlexJsonCasDeserializer.java
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.flexjson;
+
+import static com.fasterxml.jackson.core.JsonTokenId.ID_END_ARRAY;
+import static com.fasterxml.jackson.core.JsonTokenId.ID_END_OBJECT;
+import static com.fasterxml.jackson.core.JsonTokenId.ID_START_ARRAY;
+import static com.fasterxml.jackson.core.JsonTokenId.ID_START_OBJECT;
+import static com.fasterxml.jackson.core.JsonTokenId.ID_STRING;
+import static org.apache.uima.cas.CAS.FEATURE_BASE_NAME_LANGUAGE;
+import static org.apache.uima.cas.CAS.FEATURE_BASE_NAME_SOFAID;
+import static org.apache.uima.cas.CAS.FEATURE_BASE_NAME_SOFASTRING;
+import static org.apache.uima.cas.CAS.TYPE_NAME_INTEGER;
+import static org.apache.uima.cas.CAS.TYPE_NAME_SOFA;
+import static org.apache.uima.cas.CAS.TYPE_NAME_STRING;
+import static org.apache.uima.json.flexjson.FlexJsonNames.FEATURE_STRUCTURES_FIELD;
+import static org.apache.uima.json.flexjson.FlexJsonNames.FLAG_DOCUMENT_ANNOTATION;
+import static org.apache.uima.json.flexjson.FlexJsonNames.TYPES_FIELD;
+import static org.apache.uima.json.flexjson.FlexJsonNames.VIEWS_FIELD;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Feature;
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.cas.Type;
+import org.apache.uima.json.flexjson.model.Json2FeatureStructure;
+import org.apache.uima.json.flexjson.model.Json2Type;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.type.TypeReference;
+
+@Deprecated
+public class FlexJsonCasDeserializer {
+  private final JsonParser parser;
+
+  private Map<String, Json2Type> types;
+
+  private Map<String, CAS> views;
+
+  public FlexJsonCasDeserializer(JsonParser aParser) {
+    parser = aParser;
+  }
+
+  public void read(CAS aCas) throws IOException {
+    views = new HashMap<>();
+
+    aCas.getViewIterator().forEachRemaining(view -> views.put(view.getViewName(), view));
+
+    boolean isFirst = true;
+
+    Map<String, Json2FeatureStructure> jfses = null;
+
+    while (parser.nextToken() != null) {
+      if (isFirst) {
+        isFirst = false;
+        switch (parser.currentTokenId()) {
+          case ID_STRING:
+            readDocumentText(aCas, parser.getValueAsString());
+            return;
+          case ID_START_ARRAY:
+            jfses = readFeatureStructuresAsArray(parser);
+            return;
+          case ID_START_OBJECT:
+            // In this case, we continue in the main loop and process the type system and
+            // feature structures if we find them.
+            parser.nextFieldName();
+            break;
+          default:
+            throw new IOException("JSON must start with an object, array or string value");
+        }
+      }
+
+      if (parser.currentTokenId() == ID_END_ARRAY || parser.currentTokenId() == ID_END_OBJECT) {
+        break;
+      }
+
+      // If we get here, we are operating on an object-type representation of the full CAS
+      switch (parser.getValueAsString()) {
+        case TYPES_FIELD:
+          parser.nextValue();
+          types = readTypeSystem(parser);
+          break;
+        case VIEWS_FIELD:
+          readViews(aCas, parser);
+          break;
+        case FEATURE_STRUCTURES_FIELD:
+          switch (parser.nextToken().id()) {
+            case ID_START_ARRAY:
+              jfses = readFeatureStructuresAsArray(parser);
+              break;
+            case ID_START_OBJECT:
+              jfses = readFeatureStructuresAsObject(parser);
+              break;
+            default:
+              throw new IOException("Feature structures must be an object or array");
+          }
+          break;
+      }
+    }
+
+    if (jfses != null) {
+      for (Json2FeatureStructure jfs : jfses.values()) {
+        createFeatureStructure(aCas, jfs);
+      }
+    }
+  }
+
+  private void createFeatureStructure(CAS aCas, Json2FeatureStructure aJfs) {
+    Type type = aCas.getTypeSystem().getType(aJfs.getType());
+
+    if (TYPE_NAME_SOFA.equals(type.getName())) {
+      CAS view = getView(aCas, (String) aJfs.getFeatures().get(FEATURE_BASE_NAME_SOFAID));
+      view.setDocumentText((String) aJfs.getFeatures().get(FEATURE_BASE_NAME_SOFASTRING));
+      view.setDocumentLanguage((String) aJfs.getFeatures().get(FEATURE_BASE_NAME_LANGUAGE));
+      return;
+    }
+
+    FeatureStructure fs = getFSCreationView(aCas, aJfs).createFS(type);
+
+    if (aJfs.getFlags() != null && aJfs.getFlags().contains(FLAG_DOCUMENT_ANNOTATION)) {
+      fs.getCAS().removeFsFromIndexes(fs.getCAS().getDocumentAnnotation());
+    }
+
+    for (Entry<String, Object> entry : aJfs.getFeatures().entrySet()) {
+      Feature feature = type.getFeatureByBaseName(entry.getKey());
+      if (feature.getRange().isPrimitive()) {
+        switch (feature.getRange().getName()) {
+          case TYPE_NAME_STRING:
+            fs.setStringValue(feature, (String) entry.getValue());
+            break;
+          case TYPE_NAME_INTEGER:
+            fs.setIntValue(feature, (Integer) entry.getValue());
+            break;
+          default:
+            throw new IllegalArgumentException(
+                    "Unsupported primitive type [" + feature.getRange().getName() + "]");
+        }
+      }
+    }
+
+    addToIndexes(fs, aJfs);
+  }
+
+  private CAS getFSCreationView(CAS aCas, Json2FeatureStructure aJfs) {
+    if (aJfs.getViews().isEmpty()) {
+      return null;
+    }
+
+    if (aJfs.getViews() != null) {
+      return getView(aCas, aJfs.getViews().iterator().next());
+    }
+
+    // FIXME: case when the views are not inline but separate or when FS is simply
+    // not indexed
+    throw new UnsupportedOperationException();
+  }
+
+  private CAS getView(CAS aCas, String aViewName) {
+    CAS view = views.get(aViewName);
+    if (view == null) {
+      view = aCas.createView(aViewName);
+      views.put(aViewName, view);
+    }
+    return view;
+  }
+
+  private void addToIndexes(FeatureStructure aFS, Json2FeatureStructure aJfs) {
+    if (aJfs.getViews() != null) {
+      for (String viewName : aJfs.getViews()) {
+        getView(aFS.getCAS(), viewName).addFsToIndexes(aFS);
+      }
+    }
+  }
+
+  private void readViews(CAS aCas, JsonParser aParser) throws IOException {
+    while (aParser.currentTokenId() != ID_END_OBJECT) {
+      aParser.nextFieldName();
+      String viewName = aParser.readValueAs(String.class);
+      aCas.createView(viewName);
+    }
+  }
+
+  private Map<String, Json2Type> readTypeSystem(JsonParser aParser) throws IOException {
+    return aParser.readValueAs(TYPES_MAP_TYPE_REF);
+  }
+
+  private Map<String, Json2FeatureStructure> readFeatureStructuresAsObject(JsonParser aParser)
+          throws IOException {
+    Map<String, Json2FeatureStructure> jfses = new LinkedHashMap<>();
+    while (aParser.currentTokenId() != ID_END_OBJECT) {
+      String id = aParser.nextFieldName();
+      Json2FeatureStructure jfs = aParser.readValueAs(Json2FeatureStructure.class);
+      jfs.setId(id);
+      jfses.put(id, jfs);
+    }
+
+    return jfses;
+  }
+
+  private Map<String, Json2FeatureStructure> readFeatureStructuresAsArray(JsonParser aParser)
+          throws IOException {
+    Map<String, Json2FeatureStructure> jfses = new LinkedHashMap<>();
+    parser.nextValue();
+    while (aParser.currentTokenId() != ID_END_ARRAY) {
+      Json2FeatureStructure jfs = aParser.readValueAs(Json2FeatureStructure.class);
+      aParser.nextToken();
+      jfses.put(jfs.getId(), jfs);
+    }
+
+    return jfses;
+  }
+
+  private void readDocumentText(CAS aCas, String aString) {
+    aCas.setDocumentText(aString);
+  }
+
+  public static final TypeReference<LinkedHashMap<String, Json2Type>> TYPES_MAP_TYPE_REF = //
+          new TypeReference<LinkedHashMap<String, Json2Type>>() {
+          };
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/flexjson/FlexJsonCasSerializer.java b/uimaj-json/src/main/java/org/apache/uima/json/flexjson/FlexJsonCasSerializer.java
new file mode 100644
index 0000000..8e52262
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/flexjson/FlexJsonCasSerializer.java
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.flexjson;
+
+import static com.fasterxml.jackson.core.JsonEncoding.UTF8;
+import static java.util.Arrays.asList;
+import static java.util.Arrays.sort;
+import static java.util.Collections.sort;
+import static java.util.Collections.unmodifiableSet;
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+import static org.apache.uima.json.flexjson.FlexJsonCasSerializer.FeatureStructuresMode.AS_ARRAY;
+import static org.apache.uima.json.flexjson.FlexJsonCasSerializer.ViewsMode.INLINE;
+import static org.apache.uima.json.flexjson.FlexJsonCasSerializer.ViewsMode.SEPARATE;
+import static org.apache.uima.json.flexjson.FlexJsonNames.COMPONENT_TYPE_FIELD;
+import static org.apache.uima.json.flexjson.FlexJsonNames.FEATURE_STRUCTURES_FIELD;
+import static org.apache.uima.json.flexjson.FlexJsonNames.FLAG_DOCUMENT_ANNOTATION;
+import static org.apache.uima.json.flexjson.FlexJsonNames.ID_FIELD;
+import static org.apache.uima.json.flexjson.FlexJsonNames.REF;
+import static org.apache.uima.json.flexjson.FlexJsonNames.SUPER_TYPE_FIELD;
+import static org.apache.uima.json.flexjson.FlexJsonNames.TYPES_FIELD;
+import static org.apache.uima.json.flexjson.FlexJsonNames.TYPE_FIELD;
+import static org.apache.uima.json.flexjson.FlexJsonNames.VIEWS_FIELD;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.StreamSupport;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Feature;
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.TypeSystem;
+import org.apache.uima.cas.impl.CASImpl;
+import org.apache.uima.jcas.cas.TOP;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+@Deprecated
+public class FlexJsonCasSerializer {
+  public enum FeatureStructuresMode {
+    AS_OBJECT, AS_ARRAY
+  }
+
+  public enum ViewsMode {
+    SEPARATE, INLINE
+  }
+
+  private final Set<String> BUILT_IN_TYPES = unmodifiableSet(new HashSet<>(asList(
+          CAS.TYPE_NAME_ANNOTATION, CAS.TYPE_NAME_ANNOTATION_BASE, CAS.TYPE_NAME_ARRAY_BASE,
+          CAS.TYPE_NAME_BOOLEAN, CAS.TYPE_NAME_BOOLEAN_ARRAY, CAS.TYPE_NAME_BYTE,
+          CAS.TYPE_NAME_BYTE_ARRAY, CAS.TYPE_NAME_DOCUMENT_ANNOTATION, CAS.TYPE_NAME_DOUBLE,
+          CAS.TYPE_NAME_DOUBLE_ARRAY, CAS.TYPE_NAME_EMPTY_FLOAT_LIST, CAS.TYPE_NAME_EMPTY_FS_LIST,
+          CAS.TYPE_NAME_EMPTY_INTEGER_LIST, CAS.TYPE_NAME_EMPTY_STRING_LIST, CAS.TYPE_NAME_FLOAT,
+          CAS.TYPE_NAME_FLOAT_ARRAY, CAS.TYPE_NAME_FLOAT_LIST, CAS.TYPE_NAME_FS_ARRAY,
+          CAS.TYPE_NAME_FS_LIST, CAS.TYPE_NAME_INTEGER, CAS.TYPE_NAME_INTEGER_ARRAY,
+          CAS.TYPE_NAME_INTEGER_LIST, CAS.TYPE_NAME_LIST_BASE, CAS.TYPE_NAME_LONG,
+          CAS.TYPE_NAME_LONG_ARRAY, CAS.TYPE_NAME_NON_EMPTY_FLOAT_LIST,
+          CAS.TYPE_NAME_NON_EMPTY_FS_LIST, CAS.TYPE_NAME_NON_EMPTY_INTEGER_LIST,
+          CAS.TYPE_NAME_NON_EMPTY_STRING_LIST, CAS.TYPE_NAME_SHORT, CAS.TYPE_NAME_SHORT_ARRAY,
+          CAS.TYPE_NAME_SOFA, CAS.TYPE_NAME_STRING, CAS.TYPE_NAME_STRING_ARRAY,
+          CAS.TYPE_NAME_STRING_LIST, CAS.TYPE_NAME_TOP)));
+
+  private JsonGenerator jg;
+
+  private FeatureStructuresMode featureStructuresMode = AS_ARRAY;
+
+  private ViewsMode viewsMode = INLINE;
+
+  private Supplier<Function<FeatureStructure, String>> idRefGeneratorSupplier;
+  private Function<FeatureStructure, String> idRefGenerator;
+  private Map<FeatureStructure, String> idRefCache;
+
+  private Supplier<Function<Type, String>> typeRefGeneratorSupplier;
+  private Function<Type, String> typeRefGenerator;
+  private Map<Type, String> typeRefCache;
+
+  private Map<FeatureStructure, Set<String>> fsToViewsCache;
+
+  public FlexJsonCasSerializer(JsonGenerator aJg) {
+    jg = aJg;
+    setIdRefGeneratorSupplier(SequentialIdRefGenerator::new);
+    setTypeRefGeneratorSupplier(FullyQualifiedTypeRefGenerator::new);
+  }
+
+  public void setViewsMode(ViewsMode aViewsMode) {
+    viewsMode = aViewsMode;
+  }
+
+  public ViewsMode getViewsMode() {
+    return viewsMode;
+  }
+
+  public FeatureStructuresMode getFeatureStructuresMode() {
+    return featureStructuresMode;
+  }
+
+  public void setFeatureStructuresMode(FeatureStructuresMode aFeatureStructuresMode) {
+    featureStructuresMode = aFeatureStructuresMode;
+  }
+
+  public void setIdRefGeneratorSupplier(
+          Supplier<Function<FeatureStructure, String>> aIdRefGeneratorSupplier) {
+    idRefGeneratorSupplier = aIdRefGeneratorSupplier;
+  }
+
+  public void setTypeRefGeneratorSupplier(
+          Supplier<Function<Type, String>> aTypeRefGeneratorSupplier) {
+    typeRefGeneratorSupplier = aTypeRefGeneratorSupplier;
+  }
+
+  public void write(CAS aCas) throws IOException {
+    idRefGenerator = idRefGeneratorSupplier.get();
+    typeRefGenerator = typeRefGeneratorSupplier.get();
+
+    idRefCache = new HashMap<>();
+    typeRefCache = new HashMap<>();
+    fsToViewsCache = new IdentityHashMap<>();
+
+    jg.writeStartObject(aCas);
+
+    writeTypeSystem(aCas.getTypeSystem());
+
+    List<CAS> views = new ArrayList<>();
+    aCas.getViewIterator().forEachRemaining(views::add);
+    if (!views.isEmpty()) {
+      sort(views, comparing(CAS::getViewName));
+
+      if (viewsMode == SEPARATE) {
+        jg.writeObjectFieldStart(VIEWS_FIELD);
+        for (CAS view : views) {
+          jg.writeFieldName(view.getViewName());
+          writeView(view);
+        }
+        jg.writeEndObject();
+      }
+
+      if (viewsMode == INLINE) {
+        for (CAS view : views) {
+          for (FeatureStructure fs : view.select()) {
+            fsToViewsCache.computeIfAbsent(fs, _fs -> new HashSet<>()).add(view.getViewName());
+          }
+        }
+      }
+    }
+
+    Set<FeatureStructure> allFSes = findAllFeatureStructures(aCas);
+
+    if (!allFSes.isEmpty()) {
+      switch (featureStructuresMode) {
+        case AS_ARRAY:
+          jg.writeArrayFieldStart(FEATURE_STRUCTURES_FIELD);
+          for (FeatureStructure fs : allFSes) {
+            jg.writeStartObject(fs);
+            jg.writeStringField(ID_FIELD, fsRef(fs));
+            writeFeatureStructure(fs);
+            jg.writeEndObject();
+          }
+          jg.writeEndArray();
+          break;
+        case AS_OBJECT:
+          jg.writeObjectFieldStart(FEATURE_STRUCTURES_FIELD);
+          for (FeatureStructure fs : allFSes) {
+            jg.writeFieldName(fsRef(fs));
+            jg.writeStartObject(fs);
+            writeFeatureStructure(fs);
+            jg.writeEndObject();
+          }
+          jg.writeEndObject();
+          break;
+        default:
+          throw new IOException("Unsupported feature structures serialization mode: ["
+                  + featureStructuresMode + "]");
+      }
+    }
+
+    jg.writeEndObject();
+  }
+
+  private Set<FeatureStructure> findAllFeatureStructures(CAS aCas) {
+    Set<FeatureStructure> allFSes = new LinkedHashSet<>();
+    ((CASImpl) aCas).walkReachablePlusFSsSorted(allFSes::add, null, null, null);
+    return allFSes;
+  }
+
+  private String fsRef(FeatureStructure aFs) {
+    return idRefCache.computeIfAbsent(aFs, idRefGenerator);
+  }
+
+  private String typeRef(Type aType) {
+    return typeRefCache.computeIfAbsent(aType, typeRefGenerator);
+  }
+
+  private void writeFeatureStructure(FeatureStructure aFs) throws IOException {
+    Type type = aFs.getType();
+    jg.writeStringField(TYPE_FIELD, typeRef(type));
+
+    if (viewsMode == INLINE) {
+      Set<String> views = fsToViewsCache.get(aFs);
+
+      if (views != null && !views.isEmpty()) {
+        String[] viewsArray = views.toArray(new String[views.size()]);
+        sort(viewsArray);
+        jg.writeArrayFieldStart(VIEWS_FIELD);
+        for (String view : viewsArray) {
+          jg.writeString(view);
+        }
+        jg.writeEndArray();
+      }
+    }
+
+    List<String> flags = new ArrayList<>();
+    if (((CASImpl) aFs.getCAS()).getDocumentAnnotationNoCreate() == aFs) {
+      flags.add(FLAG_DOCUMENT_ANNOTATION);
+    }
+
+    if (!flags.isEmpty()) {
+      jg.writeArrayFieldStart(FlexJsonNames.FLAGS_FIELD);
+      for (String flag : flags) {
+        jg.writeString(flag);
+      }
+      jg.writeEndArray();
+    }
+
+    for (Feature feature : type.getFeatures()) {
+      writeFeature(aFs, feature);
+    }
+  }
+
+  private void writeFeature(FeatureStructure aFs, Feature aFeature) throws IOException {
+    if (!aFeature.getRange().isPrimitive()) {
+      FeatureStructure target = aFs.getFeatureValue(aFeature);
+      if (target != null) {
+        jg.writeFieldName(REF + aFeature.getShortName());
+        jg.writeString(fsRef(aFs.getFeatureValue(aFeature)));
+      }
+      return;
+    }
+
+    if (aFeature.getRange().isStringOrStringSubtype()) {
+      String value = aFs.getStringValue(aFeature);
+      if (value != null) {
+        jg.writeFieldName(aFeature.getShortName());
+        jg.writeString(value);
+      }
+
+      return;
+    }
+
+    jg.writeFieldName(aFeature.getShortName());
+    String rangeTypeName = aFeature.getRange().getName();
+    switch (rangeTypeName) {
+      case CAS.TYPE_NAME_INTEGER:
+        jg.writeNumber(aFs.getIntValue(aFeature));
+        break;
+      case CAS.TYPE_NAME_BOOLEAN:
+        jg.writeBoolean(aFs.getBooleanValue(aFeature));
+        break;
+      default:
+        throw new IOException("Unsupported primitive type [" + rangeTypeName + "]");
+    }
+  }
+
+  private void writeView(CAS aView) throws IOException {
+    jg.writeStartArray();
+
+    jg.writeString(fsRef(aView.getSofa()));
+    for (TOP fs : aView.getIndexedFSs()) {
+      jg.writeString(fsRef(fs));
+    }
+
+    jg.writeEndArray();
+  }
+
+  private void writeTypeSystem(TypeSystem aTypeSystem) throws IOException {
+    List<Type> types = StreamSupport.stream(aTypeSystem.spliterator(), false)
+            .sorted(comparing(Type::getName))
+            .filter(type -> !BUILT_IN_TYPES.contains(type.getName())).collect(toList());
+
+    if (types.isEmpty()) {
+      return;
+    }
+
+    jg.writeFieldName(TYPES_FIELD);
+
+    jg.writeStartObject(aTypeSystem);
+
+    for (Type type : types) {
+      writeType(aTypeSystem, type);
+    }
+
+    jg.writeEndObject();
+  }
+
+  private void writeType(TypeSystem aTypeSystem, Type aType) throws IOException {
+    jg.writeFieldName(typeRef(aType));
+
+    jg.writeStartObject(aType);
+
+    if (!typeRef(aType).equals(aType.getName())) {
+      jg.writeStringField(FlexJsonNames.NAME_FIELD, aType.getName());
+    }
+
+    Type parent = aTypeSystem.getParent(aType);
+    if (parent != null) {
+      jg.writeStringField(SUPER_TYPE_FIELD, parent.getName());
+    }
+
+    if (aType.getComponentType() != null) {
+      jg.writeStringField(COMPONENT_TYPE_FIELD, aType.getComponentType().getName());
+    }
+
+    List<Feature> newFeatures = aType.getFeatures().stream().filter(f -> f.getDomain() == aType)
+            .collect(toList());
+    if (!newFeatures.isEmpty()) {
+      for (Feature feature : newFeatures) {
+        jg.writeStringField(feature.getShortName(), feature.getRange().getName());
+      }
+    }
+
+    jg.writeEndObject();
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private Supplier<Function<FeatureStructure, String>> idRefGeneratorSupplier;
+    private Supplier<Function<Type, String>> typeRefGeneratorSupplier;
+    private FeatureStructuresMode featureStructuresMode = AS_ARRAY;
+    private ViewsMode viewsMode = INLINE;
+
+    public Builder() {
+      setIdRefGeneratorSupplier(SequentialIdRefGenerator::new);
+      setTypeRefGeneratorSupplier(FullyQualifiedTypeRefGenerator::new);
+    }
+
+    public Builder setFeatureStructuresMode(FeatureStructuresMode aFeatureStructuresMode) {
+      featureStructuresMode = aFeatureStructuresMode;
+      return this;
+    }
+
+    public Builder setViewsMode(ViewsMode aViewsMode) {
+      viewsMode = aViewsMode;
+      return this;
+    }
+
+    public Builder setIdRefGeneratorSupplier(
+            Supplier<Function<FeatureStructure, String>> aIdRefGeneratorSupplier) {
+      idRefGeneratorSupplier = aIdRefGeneratorSupplier;
+      return this;
+    }
+
+    public Builder setTypeRefGeneratorSupplier(
+            Supplier<Function<Type, String>> aTypeRefGeneratorSupplier) {
+      typeRefGeneratorSupplier = aTypeRefGeneratorSupplier;
+      return this;
+    }
+
+    public FlexJsonCasSerializer build(JsonGenerator jg) {
+      FlexJsonCasSerializer ser = new FlexJsonCasSerializer(jg);
+      ser.setFeatureStructuresMode(featureStructuresMode);
+      ser.setViewsMode(viewsMode);
+      ser.setIdRefGeneratorSupplier(idRefGeneratorSupplier);
+      ser.setTypeRefGeneratorSupplier(typeRefGeneratorSupplier);
+      return ser;
+    }
+
+    public void write(CAS aCas, File aTargetFile) throws IOException {
+      JsonFactory jsonFactory = new JsonFactory();
+      jsonFactory.setCodec(new ObjectMapper());
+      try (JsonGenerator jg = jsonFactory.createGenerator(aTargetFile, UTF8)
+              .useDefaultPrettyPrinter()) {
+        FlexJsonCasSerializer ser = build(jg);
+        ser.write(aCas);
+      }
+    }
+  }
+
+  public static void write(CAS aCas, File aTargetFile) throws IOException {
+    JsonFactory jsonFactory = new JsonFactory();
+    jsonFactory.setCodec(new ObjectMapper());
+    try (JsonGenerator jg = jsonFactory.createGenerator(aTargetFile, UTF8)
+            .useDefaultPrettyPrinter()) {
+      FlexJsonCasSerializer ser = new FlexJsonCasSerializer(jg);
+      ser.write(aCas);
+    }
+  }
+
+  public static class SequentialIdRefGenerator implements Function<FeatureStructure, String> {
+    private int nextId = 0;
+
+    @Override
+    public String apply(FeatureStructure aT) {
+      return String.valueOf(nextId++);
+    }
+  }
+
+  public static class FullyQualifiedTypeRefGenerator implements Function<Type, String> {
+    @Override
+    public String apply(Type aType) {
+      return aType.getName();
+    }
+  }
+
+  public static class ShortTypeRefGenerator implements Function<Type, String> {
+    private Set<String> usedNames = new HashSet<>();
+
+    @Override
+    public String apply(Type aType) {
+      if (!usedNames.contains(aType.getShortName())) {
+        usedNames.add(aType.getShortName());
+        return aType.getShortName();
+      }
+
+      int n = 1;
+      String newName;
+      while (usedNames.contains(newName = aType.getShortName() + "-" + n)) {
+        n++;
+      }
+
+      usedNames.add(newName);
+      return newName;
+    }
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/flexjson/FlexJsonNames.java b/uimaj-json/src/main/java/org/apache/uima/json/flexjson/FlexJsonNames.java
new file mode 100644
index 0000000..2590836
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/flexjson/FlexJsonNames.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.flexjson;
+
+@Deprecated
+public class FlexJsonNames {
+  private static final String SPECIAL = "%";
+
+  public static final String TYPE_FIELD = SPECIAL + "TYPE";
+
+  public static final String TYPES_FIELD = SPECIAL + "TYPES";
+
+  public static final String FEATURES_FIELD = SPECIAL + "FEATURES";
+
+  public static final String VIEWS_FIELD = SPECIAL + "VIEWS";
+
+  public static final String FEATURE_STRUCTURES_FIELD = SPECIAL + "FEATURE_STRUCTURES";
+
+  public static final String REF = "@";
+
+  public static final String NAME_FIELD = SPECIAL + "NAME";
+
+  public static final String SUPER_TYPE_FIELD = SPECIAL + "SUPER_TYPE";
+
+  public static final String COMPONENT_TYPE_FIELD = SPECIAL + "COMPONENT_TYPE";
+
+  public static final String ID_FIELD = SPECIAL + "ID";
+
+  public static final String FLAGS_FIELD = SPECIAL + "FLAGS";
+
+  public static final String FLAG_DOCUMENT_ANNOTATION = "DocumentAnnotation";
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/flexjson/model/Json2FeatureStructure.java b/uimaj-json/src/main/java/org/apache/uima/json/flexjson/model/Json2FeatureStructure.java
new file mode 100644
index 0000000..7b3ab19
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/flexjson/model/Json2FeatureStructure.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.flexjson.model;
+
+import static org.apache.uima.cas.CAS.TYPE_NAME_ANNOTATION;
+import static org.apache.uima.json.flexjson.FlexJsonNames.FLAGS_FIELD;
+import static org.apache.uima.json.flexjson.FlexJsonNames.ID_FIELD;
+import static org.apache.uima.json.flexjson.FlexJsonNames.TYPE_FIELD;
+import static org.apache.uima.json.flexjson.FlexJsonNames.VIEWS_FIELD;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+
+import org.apache.uima.json.jsoncas2.JsonCas2Names;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+@Deprecated
+@JsonPropertyOrder(value = { "id", "type", "views" })
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Json2FeatureStructure {
+  @JsonProperty(value = ID_FIELD, required = false)
+  private String id;
+
+  @JsonProperty(value = TYPE_FIELD, required = true, defaultValue = TYPE_NAME_ANNOTATION)
+  private String type;
+
+  @JsonProperty(value = FLAGS_FIELD, required = false)
+  private LinkedHashSet<String> flags;
+
+  @JsonProperty(value = VIEWS_FIELD, required = false)
+  private LinkedHashSet<String> views;
+
+  private Map<String, Object> features = new LinkedHashMap<>();
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String aId) {
+    id = aId;
+  }
+
+  public String getType() {
+    return type;
+  }
+
+  public void setType(String aType) {
+    type = aType;
+  }
+
+  public LinkedHashSet<String> getViews() {
+    return views;
+  }
+
+  public void setViews(LinkedHashSet<String> aViews) {
+    views = aViews;
+  }
+
+  public LinkedHashSet<String> getFlags() {
+    return flags;
+  }
+
+  public void setFlags(LinkedHashSet<String> aFlags) {
+    flags = aFlags;
+  }
+
+  @JsonAnySetter
+  public void setFeature(String name, Object value) {
+    if (name.startsWith(JsonCas2Names.REF_FEATURE_PREFIX)) {
+      name = name.substring(1);
+    }
+
+    features.put(name, value);
+  }
+
+  @JsonAnyGetter
+  public Map<String, Object> getFeatures() {
+    return features;
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/flexjson/model/Json2Type.java b/uimaj-json/src/main/java/org/apache/uima/json/flexjson/model/Json2Type.java
new file mode 100644
index 0000000..e418ac5
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/flexjson/model/Json2Type.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.flexjson.model;
+
+import static org.apache.uima.cas.CAS.TYPE_NAME_ANNOTATION;
+import static org.apache.uima.json.flexjson.FlexJsonNames.COMPONENT_TYPE_FIELD;
+import static org.apache.uima.json.flexjson.FlexJsonNames.NAME_FIELD;
+import static org.apache.uima.json.flexjson.FlexJsonNames.SUPER_TYPE_FIELD;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+@Deprecated
+@JsonPropertyOrder(value = { "name", "parent", "componentType" })
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Json2Type {
+  @JsonProperty(value = NAME_FIELD, required = false)
+  private String name;
+
+  @JsonProperty(value = SUPER_TYPE_FIELD, required = true, defaultValue = TYPE_NAME_ANNOTATION)
+  private String parent;
+
+  @JsonProperty(value = COMPONENT_TYPE_FIELD, required = false)
+  private String componentType;
+
+  private Map<String, String> features = new LinkedHashMap<>();
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String aName) {
+    name = aName;
+  }
+
+  public String getParent() {
+    return parent;
+  }
+
+  public void setParent(String aParent) {
+    parent = aParent;
+  }
+
+  public String getComponentType() {
+    return componentType;
+  }
+
+  public void setComponentType(String aComponentType) {
+    componentType = aComponentType;
+  }
+
+  @JsonAnySetter
+  public void setProperty(String name, Object value) {
+    if (value instanceof String) {
+      features.put(name, (String) value);
+      return;
+    }
+
+    throw new IllegalArgumentException("Feature type names must be strings");
+  }
+
+  @SuppressWarnings("unchecked")
+  @JsonAnyGetter
+  public Map<String, Object> getProperties() {
+    return (Map) features;
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/JsonCas2Deserializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/JsonCas2Deserializer.java
new file mode 100644
index 0000000..ee95e37
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/JsonCas2Deserializer.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.json.jsoncas2.mode.FeatureStructuresMode;
+import org.apache.uima.json.jsoncas2.model.FeatureStructures;
+import org.apache.uima.json.jsoncas2.model.Views;
+import org.apache.uima.json.jsoncas2.ser.CasDeserializer;
+import org.apache.uima.json.jsoncas2.ser.FeatureDeserializer;
+import org.apache.uima.json.jsoncas2.ser.FeatureStructureDeserializer;
+import org.apache.uima.json.jsoncas2.ser.FeatureStructuresAsArrayDeserializer;
+import org.apache.uima.json.jsoncas2.ser.FeatureStructuresAsObjectDeserializer;
+import org.apache.uima.json.jsoncas2.ser.TypeDeserializer;
+import org.apache.uima.json.jsoncas2.ser.TypeSystemDeserializer;
+import org.apache.uima.json.jsoncas2.ser.ViewsDeserializer;
+import org.apache.uima.resource.metadata.FeatureDescription;
+import org.apache.uima.resource.metadata.TypeDescription;
+import org.apache.uima.resource.metadata.TypeSystemDescription;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+public class JsonCas2Deserializer {
+  private FeatureStructuresMode fsMode = FeatureStructuresMode.AS_ARRAY;
+  private ObjectMapper cachedMapper;
+
+  public void setFsMode(FeatureStructuresMode aFsMode) {
+    fsMode = aFsMode;
+  }
+
+  public FeatureStructuresMode getFsMode() {
+    return fsMode;
+  }
+
+  private synchronized ObjectMapper getMapper() {
+    if (cachedMapper == null) {
+      SimpleModule module = new SimpleModule("UIMA CAS JSON",
+              new Version(1, 0, 0, null, null, null));
+
+      module.addDeserializer(CAS.class, new CasDeserializer());
+      module.addDeserializer(FeatureStructure.class, new FeatureStructureDeserializer());
+
+      switch (fsMode) {
+        case AS_ARRAY:
+          module.addDeserializer(FeatureStructures.class,
+                  new FeatureStructuresAsArrayDeserializer());
+          break;
+        case AS_OBJECT:
+          module.addDeserializer(FeatureStructures.class,
+                  new FeatureStructuresAsObjectDeserializer());
+          break;
+      }
+
+      module.addDeserializer(FeatureDescription.class, new FeatureDeserializer());
+      module.addDeserializer(TypeDescription.class, new TypeDeserializer());
+      module.addDeserializer(TypeSystemDescription.class, new TypeSystemDeserializer());
+      module.addDeserializer(Views.class, new ViewsDeserializer());
+
+      cachedMapper = new ObjectMapper();
+      cachedMapper.registerModule(module);
+    }
+    return cachedMapper;
+  }
+
+  public void deserialize(File aSourceFile, CAS aTargetCas) throws IOException {
+    getMapper().reader().forType(CAS.class) //
+            .withAttribute(CasDeserializer.CONTEXT_CAS, aTargetCas) //
+            .readValue(aSourceFile);
+  }
+
+  public void deserialize(InputStream aSourceStream, CAS aTargetCas) throws IOException {
+    getMapper().reader().forType(CAS.class) //
+            .withAttribute(CasDeserializer.CONTEXT_CAS, aTargetCas) //
+            .readValue(aSourceStream);
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/JsonCas2Names.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/JsonCas2Names.java
new file mode 100644
index 0000000..81c725b
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/JsonCas2Names.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.impl.TypeImpl;
+import org.apache.uima.resource.metadata.FeatureDescription;
+import org.apache.uima.resource.metadata.TypeDescription;
+
+public class JsonCas2Names {
+  public static final String RESERVED_FIELD_PREFIX = "%";
+
+  public static final String TYPE_FIELD = RESERVED_FIELD_PREFIX + "TYPE";
+
+  public static final String RANGE_FIELD = RESERVED_FIELD_PREFIX + "RANGE";
+
+  public static final String HEADER_FIELD = RESERVED_FIELD_PREFIX + "HEADER";
+
+  public static final String TYPES_FIELD = RESERVED_FIELD_PREFIX + "TYPES";
+
+  public static final String FEATURES_FIELD = RESERVED_FIELD_PREFIX + "FEATURES";
+
+  public static final String VIEWS_FIELD = RESERVED_FIELD_PREFIX + "VIEWS";
+
+  /**
+   * @see CAS#getSofa()
+   */
+  public static final String VIEW_SOFA_FIELD = RESERVED_FIELD_PREFIX + "SOFA";
+
+  public static final String VIEW_MEMBERS_FIELD = RESERVED_FIELD_PREFIX + "MEMBERS";
+
+  public static final String FEATURE_STRUCTURES_FIELD = RESERVED_FIELD_PREFIX
+          + "FEATURE_STRUCTURES";
+
+  public static final String REF_FEATURE_PREFIX = "@";
+
+  public static final String NUMERIC_FEATURE_PREFIX = "#";
+
+  public static final String ANCHOR_FEATURE_PREFIX = "^";
+
+  public static final String NAME_FIELD = RESERVED_FIELD_PREFIX + "NAME";
+
+  /**
+   * @see TypeDescription#getSupertypeName()
+   * @see TypeImpl#getSuperType()
+   */
+  public static final String SUPER_TYPE_FIELD = RESERVED_FIELD_PREFIX + "SUPER_TYPE";
+
+  /**
+   * @see TypeDescription#getDescription()
+   */
+  public static final String DESCRIPTION_FIELD = RESERVED_FIELD_PREFIX + "DESCRIPTION";
+
+  /**
+   * @see FeatureDescription#getElementType()
+   * @see Type#getComponentType()
+   */
+  public static final String ELEMENT_TYPE_FIELD = RESERVED_FIELD_PREFIX + "ELEMENT_TYPE";
+
+  public static final String MULTIPLE_REFERENCES_ALLOWED_FIELD = RESERVED_FIELD_PREFIX
+          + "MULTIPLE_REFERENCES_ALLOWED";
+
+  public static final String ID_FIELD = RESERVED_FIELD_PREFIX + "ID";
+
+  // public static final String FLAGS_FIELD = RESERVED_FIELD_PREFIX + "FLAGS";
+
+  public static final String FLAG_DOCUMENT_ANNOTATION = "DocumentAnnotation";
+
+  public static final String ARRAY_SUFFIX = "[]";
+
+  public static final String ELEMENTS_FIELD = RESERVED_FIELD_PREFIX + "ELEMENTS";
+
+  public static final String HEADER_OFFSET_ENCODING = "offset-encoding";
+
+  public static final String NUMBER_FLOAT_NAN = "NaN";
+
+  public static final String NUMBER_FLOAT_POSITIVE_INFINITY = "Infinity";
+
+  public static final String NUMBER_FLOAT_POSITIVE_INFINITY_ABBR = "Inf";
+
+  public static final String NUMBER_FLOAT_NEGATIVE_INFINITY = "-Infinity";
+
+  public static final String NUMBER_FLOAT_NEGATIVE_INFINITY_ABBR = "-Inf";
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/JsonCas2Serializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/JsonCas2Serializer.java
new file mode 100644
index 0000000..978b809
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/JsonCas2Serializer.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2;
+
+import static org.apache.uima.json.jsoncas2.mode.FeatureStructuresMode.AS_ARRAY;
+import static org.apache.uima.json.jsoncas2.mode.OffsetConversionMode.UTF_16;
+import static org.apache.uima.json.jsoncas2.mode.SofaMode.AS_REGULAR_FEATURE_STRUCTURE;
+import static org.apache.uima.json.jsoncas2.mode.TypeSystemMode.FULL;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.function.ToIntFunction;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.cas.Type;
+import org.apache.uima.json.jsoncas2.mode.FeatureStructuresMode;
+import org.apache.uima.json.jsoncas2.mode.OffsetConversionMode;
+import org.apache.uima.json.jsoncas2.mode.SofaMode;
+import org.apache.uima.json.jsoncas2.mode.TypeSystemMode;
+import org.apache.uima.json.jsoncas2.ref.FullyQualifiedTypeRefGenerator;
+import org.apache.uima.json.jsoncas2.ref.ReferenceCache;
+import org.apache.uima.json.jsoncas2.ref.SequentialIdRefGenerator;
+import org.apache.uima.json.jsoncas2.ser.CasSerializer;
+import org.apache.uima.json.jsoncas2.ser.CommonArrayFSSerializer;
+import org.apache.uima.json.jsoncas2.ser.FeatureSerializer;
+import org.apache.uima.json.jsoncas2.ser.FeatureStructureSerializer;
+import org.apache.uima.json.jsoncas2.ser.FeatureStructuresAsArraySerializer;
+import org.apache.uima.json.jsoncas2.ser.FeatureStructuresAsObjectSerializer;
+import org.apache.uima.json.jsoncas2.ser.SofaSerializer;
+import org.apache.uima.json.jsoncas2.ser.TypeSerializer;
+import org.apache.uima.json.jsoncas2.ser.TypeSystemSerializer;
+import org.apache.uima.json.jsoncas2.ser.ViewsSerializer;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+public class JsonCas2Serializer {
+
+  private FeatureStructuresMode fsMode = AS_ARRAY;
+  private SofaMode sofaMode = AS_REGULAR_FEATURE_STRUCTURE;
+  private TypeSystemMode typeSystemMode = FULL;
+  private OffsetConversionMode offsetConversionMode = UTF_16;
+  private ObjectMapper cachedMapper;
+  private Supplier<ToIntFunction<FeatureStructure>> idRefGeneratorSupplier = SequentialIdRefGenerator::new;
+  private Supplier<Function<Type, String>> typeRefGeneratorSupplier = FullyQualifiedTypeRefGenerator::new;
+
+  public void setFsMode(FeatureStructuresMode aFsMode) {
+    fsMode = aFsMode;
+  }
+
+  public FeatureStructuresMode getFsMode() {
+    return fsMode;
+  }
+
+  public void setSofaMode(SofaMode aSofaMode) {
+    sofaMode = aSofaMode;
+  }
+
+  public SofaMode getSofaMode() {
+    return sofaMode;
+  }
+
+  public void setOffsetConversionMode(OffsetConversionMode aOffsetConversionMode) {
+    offsetConversionMode = aOffsetConversionMode;
+  }
+
+  public OffsetConversionMode getOffsetConversionMode() {
+    return offsetConversionMode;
+  }
+
+  public void setIdRefGeneratorSupplier(
+          Supplier<ToIntFunction<FeatureStructure>> aIdRefGeneratorSupplier) {
+    idRefGeneratorSupplier = aIdRefGeneratorSupplier;
+  }
+
+  public Supplier<ToIntFunction<FeatureStructure>> getIdRefGeneratorSupplier() {
+    return idRefGeneratorSupplier;
+  }
+
+  public void setTypeRefGeneratorSupplier(
+          Supplier<Function<Type, String>> aTypeRefGeneratorSupplier) {
+    typeRefGeneratorSupplier = aTypeRefGeneratorSupplier;
+  }
+
+  public Supplier<Function<Type, String>> getTypeRefGeneratorSupplier() {
+    return typeRefGeneratorSupplier;
+  }
+
+  public void setTypeSystemMode(TypeSystemMode aMode) {
+    typeSystemMode = aMode;
+  }
+
+  public TypeSystemMode getTypeSystemMode() {
+    return typeSystemMode;
+  }
+
+  private synchronized ObjectMapper getMapper() {
+    if (cachedMapper == null) {
+      SimpleModule module = new SimpleModule("UIMA CAS JSON",
+              new Version(1, 0, 0, null, null, null));
+
+      ReferenceCache.Builder refCacheBuilder = ReferenceCache.builder()
+              .withIdRefGeneratorSupplier(idRefGeneratorSupplier)
+              .withTypeRefGeneratorSupplier(typeRefGeneratorSupplier);
+      module.addSerializer(new CasSerializer(refCacheBuilder::build));
+      module.addSerializer(new TypeSystemSerializer());
+      module.addSerializer(new TypeSerializer());
+      module.addSerializer(new FeatureSerializer());
+      module.addSerializer(new CommonArrayFSSerializer());
+
+      switch (sofaMode) {
+        case AS_PART_OF_VIEW:
+          module.addSerializer(new SofaSerializer());
+          break;
+        case AS_REGULAR_FEATURE_STRUCTURE:
+          // Nothing to do
+          break;
+      }
+
+      module.addSerializer(new FeatureStructureSerializer());
+
+      switch (fsMode) {
+        case AS_ARRAY:
+          module.addSerializer(new FeatureStructuresAsArraySerializer());
+          break;
+        case AS_OBJECT:
+          module.addSerializer(new FeatureStructuresAsObjectSerializer());
+          break;
+      }
+
+      module.addSerializer(new ViewsSerializer());
+
+      cachedMapper = new ObjectMapper();
+      cachedMapper.registerModule(module);
+    }
+
+    return cachedMapper;
+  }
+
+  private ObjectWriter getWriter() {
+    return getMapper().writerWithDefaultPrettyPrinter() //
+            .withAttribute(SofaMode.KEY, sofaMode) //
+            .withAttribute(FeatureStructuresMode.KEY, fsMode)
+            .withAttribute(OffsetConversionMode.KEY, offsetConversionMode)
+            .withAttribute(TypeSystemMode.KEY, typeSystemMode);
+  }
+
+  public void serialize(CAS aCas, File aTargetFile) throws IOException {
+    getWriter().writeValue(aTargetFile, aCas);
+  }
+
+  public void serialize(CAS aCas, OutputStream aTargetStream) throws IOException {
+    getWriter().writeValue(aTargetStream, aCas);
+  }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/encoding/OffsetConverter.java
similarity index 60%
copy from uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
copy to uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/encoding/OffsetConverter.java
index 6ba69f5..a273cf3 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/encoding/OffsetConverter.java
@@ -16,18 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.uima.cas.serdes;
+package org.apache.uima.json.jsoncas2.encoding;
 
-import static org.junit.jupiter.api.Assumptions.assumeFalse;
+public interface OffsetConverter {
+  int mapExternal(int aOffset);
 
-import java.util.regex.Pattern;
-
-public class SerDesAssuptions {
-  public static void assumeNotKnownToFail(Runnable aScenario, String... aPatternsAndReasons) {
-    for (int i = 0; i < aPatternsAndReasons.length; i += 2) {
-      String pattern = aPatternsAndReasons[i];
-      String reason = aPatternsAndReasons[i + 1];
-      assumeFalse(Pattern.matches(pattern, aScenario.toString()), "Skipped because: " + reason);
-    }
-  }
+  int mapInternal(int aOffset);
 }
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/encoding/Utf16CodeunitOffsetConverter.java
similarity index 60%
copy from uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
copy to uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/encoding/Utf16CodeunitOffsetConverter.java
index 6ba69f5..f09f9ca 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/encoding/Utf16CodeunitOffsetConverter.java
@@ -16,18 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.uima.cas.serdes;
+package org.apache.uima.json.jsoncas2.encoding;
 
-import static org.junit.jupiter.api.Assumptions.assumeFalse;
+public class Utf16CodeunitOffsetConverter implements OffsetConverter {
+  public Utf16CodeunitOffsetConverter(String aString) {
+    // NOP
+  }
 
-import java.util.regex.Pattern;
+  @Override
+  public int mapExternal(int aOffset) {
+    return aOffset;
+  }
 
-public class SerDesAssuptions {
-  public static void assumeNotKnownToFail(Runnable aScenario, String... aPatternsAndReasons) {
-    for (int i = 0; i < aPatternsAndReasons.length; i += 2) {
-      String pattern = aPatternsAndReasons[i];
-      String reason = aPatternsAndReasons[i + 1];
-      assumeFalse(Pattern.matches(pattern, aScenario.toString()), "Skipped because: " + reason);
-    }
+  @Override
+  public int mapInternal(int aOffset) {
+    return aOffset;
   }
 }
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/encoding/Utf32CodepointOffsetConverter.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/encoding/Utf32CodepointOffsetConverter.java
new file mode 100644
index 0000000..e4fa981
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/encoding/Utf32CodepointOffsetConverter.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.encoding;
+
+import static java.util.Arrays.fill;
+
+public class Utf32CodepointOffsetConverter implements OffsetConverter {
+  public static final int UNMAPPED = Integer.MIN_VALUE;
+
+  private final int[] internalToExternal;
+  private final int[] externalToInternal;
+
+  public Utf32CodepointOffsetConverter(String aString) {
+    if (aString == null) {
+      internalToExternal = null;
+      externalToInternal = null;
+      return;
+    }
+
+    int codePointCount = aString.codePointCount(0, aString.length());
+    externalToInternal = new int[codePointCount + 1];
+    fill(externalToInternal, UNMAPPED);
+
+    int codeUnitCount = aString.length();
+    internalToExternal = new int[codeUnitCount + 1];
+    fill(internalToExternal, UNMAPPED);
+
+    int cpi = 0;
+    int cui = 0;
+    while (cui < aString.length()) {
+      int cp = aString.codePointAt(cui);
+      externalToInternal[cpi] = cui;
+      internalToExternal[cui] = cpi;
+
+      cpi++;
+      cui += Character.charCount(cp);
+    }
+
+    externalToInternal[cpi] = cui;
+    internalToExternal[cui] = cpi;
+  }
+
+  @Override
+  public int mapExternal(int aOffset) {
+    if (externalToInternal == null) {
+      return aOffset;
+    }
+
+    if (aOffset >= externalToInternal.length) {
+      return UNMAPPED;
+    }
+
+    return externalToInternal[aOffset];
+  }
+
+  @Override
+  public int mapInternal(int aOffset) {
+    if (internalToExternal == null) {
+      return aOffset;
+    }
+
+    if (aOffset >= internalToExternal.length) {
+      return UNMAPPED;
+    }
+
+    return internalToExternal[aOffset];
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/encoding/Utf8ByteOffsetConverter.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/encoding/Utf8ByteOffsetConverter.java
new file mode 100644
index 0000000..695602c
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/encoding/Utf8ByteOffsetConverter.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.encoding;
+
+import static java.lang.Character.charCount;
+import static java.lang.Character.toChars;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.fill;
+
+public class Utf8ByteOffsetConverter implements OffsetConverter {
+  public static final int UNMAPPED = Integer.MIN_VALUE;
+
+  private final int[] internalToExternal;
+  private final int[] externalToInternal;
+
+  public Utf8ByteOffsetConverter(String aString) {
+    if (aString == null) {
+      internalToExternal = null;
+      externalToInternal = null;
+      return;
+    }
+
+    int byteCount = aString.getBytes(UTF_8).length;
+    externalToInternal = new int[byteCount + 1];
+    fill(externalToInternal, UNMAPPED);
+
+    int codeUnitCount = aString.length();
+    internalToExternal = new int[codeUnitCount + 1];
+    fill(internalToExternal, UNMAPPED);
+
+    int cbi = 0;
+    int cui = 0;
+    while (cui < aString.length()) {
+      externalToInternal[cbi] = cui;
+      internalToExternal[cui] = cbi;
+
+      int cp = aString.codePointAt(cui);
+      cbi += String.valueOf(toChars(cp)).getBytes(UTF_8).length;
+      cui += charCount(cp);
+    }
+
+    externalToInternal[cbi] = cui;
+    internalToExternal[cui] = cbi;
+  }
+
+  @Override
+  public int mapExternal(int aOffset) {
+    if (externalToInternal == null) {
+      return aOffset;
+    }
+
+    if (aOffset >= externalToInternal.length) {
+      return UNMAPPED;
+    }
+
+    return externalToInternal[aOffset];
+  }
+
+  @Override
+  public int mapInternal(int aOffset) {
+    if (internalToExternal == null) {
+      return aOffset;
+    }
+
+    if (aOffset >= internalToExternal.length) {
+      return UNMAPPED;
+    }
+
+    return internalToExternal[aOffset];
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/ArrayTypeMode.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/ArrayTypeMode.java
new file mode 100644
index 0000000..7683f6c
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/ArrayTypeMode.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.mode;
+
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+public enum ArrayTypeMode {
+  /**
+   * Serialize array fields using the the compact {@code TypeName[]} syntax.
+   */
+  AS_ARRAY_TYPED_RANGE,
+
+  /**
+   * Serialize array fields using the traditional {@code range} and {@code elementType} separation
+   * that we know from the XML type system descriptors.
+   */
+  AS_RANGE_AND_ELEMENT;
+
+  public static final String KEY = "UIMA.FeatureStructuresMode";
+
+  public static void set(SerializerProvider aProvider, ArrayTypeMode aMode) {
+    aProvider.setAttribute(KEY, aMode);
+  }
+
+  public static ArrayTypeMode get(SerializerProvider aProvider) {
+    ArrayTypeMode mode = (ArrayTypeMode) aProvider.getAttribute(KEY);
+    return mode != null ? mode : AS_RANGE_AND_ELEMENT;
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/FeatureStructuresMode.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/FeatureStructuresMode.java
new file mode 100644
index 0000000..65c7b35
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/FeatureStructuresMode.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.mode;
+
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+public enum FeatureStructuresMode {
+  AS_OBJECT, AS_ARRAY;
+
+  public static final String KEY = "UIMA.FeatureStructuresMode";
+
+  public static void set(SerializerProvider aProvider, FeatureStructuresMode aMode) {
+    aProvider.setAttribute(KEY, aMode);
+  }
+
+  public static FeatureStructuresMode get(SerializerProvider aProvider) {
+    FeatureStructuresMode mode = (FeatureStructuresMode) aProvider.getAttribute(KEY);
+    return mode != null ? mode : AS_ARRAY;
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/OffsetConversionMode.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/OffsetConversionMode.java
new file mode 100644
index 0000000..da20ec2
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/OffsetConversionMode.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.mode;
+
+import java.util.Optional;
+
+import org.apache.uima.json.jsoncas2.encoding.OffsetConverter;
+import org.apache.uima.json.jsoncas2.encoding.Utf16CodeunitOffsetConverter;
+import org.apache.uima.json.jsoncas2.encoding.Utf32CodepointOffsetConverter;
+import org.apache.uima.json.jsoncas2.encoding.Utf8ByteOffsetConverter;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.DatabindContext;
+
+public enum OffsetConversionMode {
+  @JsonProperty("UTF-8") //
+  UTF_8, //
+  @JsonProperty("UTF-16") //
+  UTF_16, //
+  @JsonProperty("UTF-32") //
+  UTF_32;
+
+  private static final String SEPARATOR = "::";
+  public static final String KEY = "UIMA.OffsetConversionMode";
+
+  public static void set(DatabindContext aProvider, OffsetConversionMode aMode) {
+    aProvider.setAttribute(KEY, aMode);
+  }
+
+  public static OffsetConversionMode getDefault() {
+    return UTF_16;
+  }
+
+  public static OffsetConversionMode get(DatabindContext aProvider) {
+    return (OffsetConversionMode) aProvider.getAttribute(KEY);
+  }
+
+  public static OffsetConversionMode getOrDefault(DatabindContext aProvider) {
+    OffsetConversionMode mode = get(aProvider);
+    if (mode != null) {
+      return mode;
+    }
+    return getDefault();
+  }
+
+  public static OffsetConverter initConverter(DatabindContext aProvider, String aView,
+          String aText) {
+    OffsetConverter converter;
+    switch (getOrDefault(aProvider)) {
+      case UTF_8:
+        converter = new Utf8ByteOffsetConverter(aText);
+        break;
+      case UTF_16:
+        converter = new Utf16CodeunitOffsetConverter(aText);
+        break;
+      case UTF_32:
+        converter = new Utf32CodepointOffsetConverter(aText);
+        break;
+      default:
+        throw new IllegalArgumentException("Unsupported conversion mode: [" + aProvider + "]");
+    }
+
+    aProvider.setAttribute(KEY + SEPARATOR + aView, converter);
+
+    return converter;
+  }
+
+  public static Optional<OffsetConverter> getConverter(DatabindContext aProvider, String aSofaId) {
+    return Optional.ofNullable((OffsetConverter) aProvider.getAttribute(KEY + SEPARATOR + aSofaId));
+  }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/SofaMode.java
similarity index 60%
copy from uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
copy to uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/SofaMode.java
index 6ba69f5..3e31339 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/SofaMode.java
@@ -16,18 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.uima.cas.serdes;
+package org.apache.uima.json.jsoncas2.mode;
 
-import static org.junit.jupiter.api.Assumptions.assumeFalse;
+import com.fasterxml.jackson.databind.SerializerProvider;
 
-import java.util.regex.Pattern;
+public enum SofaMode {
+  AS_REGULAR_FEATURE_STRUCTURE, AS_PART_OF_VIEW;
 
-public class SerDesAssuptions {
-  public static void assumeNotKnownToFail(Runnable aScenario, String... aPatternsAndReasons) {
-    for (int i = 0; i < aPatternsAndReasons.length; i += 2) {
-      String pattern = aPatternsAndReasons[i];
-      String reason = aPatternsAndReasons[i + 1];
-      assumeFalse(Pattern.matches(pattern, aScenario.toString()), "Skipped because: " + reason);
-    }
+  public static final String KEY = "UIMA.SofaMode";
+
+  public static void set(SerializerProvider aProvider, SofaMode aMode) {
+    aProvider.setAttribute(KEY, aMode);
+  }
+
+  public static SofaMode get(SerializerProvider aProvider) {
+    return (SofaMode) aProvider.getAttribute(KEY);
   }
 }
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/TypeSystemMode.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/TypeSystemMode.java
new file mode 100644
index 0000000..68db852
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/TypeSystemMode.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.mode;
+
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+public enum TypeSystemMode {
+
+  /**
+   * Include the full type system in the JSON file.
+   */
+  FULL,
+
+  /**
+   * Include only the types actually used.
+   */
+  MINIMAL,
+
+  /**
+   * Do not include the type system in the JSON file. The reader must obtain the type system by some
+   * other means.
+   */
+  NONE;
+
+  public static final String KEY = "UIMA.TypeSystemMode";
+
+  public static void set(SerializerProvider aProvider, TypeSystemMode aMode) {
+    aProvider.setAttribute(KEY, aMode);
+  }
+
+  public static TypeSystemMode get(SerializerProvider aProvider) {
+    TypeSystemMode mode = (TypeSystemMode) aProvider.getAttribute(KEY);
+    return mode != null ? mode : FULL;
+  }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/ViewsMode.java
similarity index 60%
copy from uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
copy to uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/ViewsMode.java
index 6ba69f5..8c155f2 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/mode/ViewsMode.java
@@ -16,18 +16,21 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.uima.cas.serdes;
+package org.apache.uima.json.jsoncas2.mode;
 
-import static org.junit.jupiter.api.Assumptions.assumeFalse;
+import com.fasterxml.jackson.databind.SerializerProvider;
 
-import java.util.regex.Pattern;
+public enum ViewsMode {
+  SEPARATE, INLINE;
 
-public class SerDesAssuptions {
-  public static void assumeNotKnownToFail(Runnable aScenario, String... aPatternsAndReasons) {
-    for (int i = 0; i < aPatternsAndReasons.length; i += 2) {
-      String pattern = aPatternsAndReasons[i];
-      String reason = aPatternsAndReasons[i + 1];
-      assumeFalse(Pattern.matches(pattern, aScenario.toString()), "Skipped because: " + reason);
-    }
+  public static final String KEY = "UIMA.ViewsMode";
+
+  public static void set(SerializerProvider aProvider, ViewsMode aMode) {
+    aProvider.setAttribute(KEY, aMode);
+  }
+
+  public static ViewsMode get(SerializerProvider aProvider) {
+    ViewsMode mode = (ViewsMode) aProvider.getAttribute(KEY);
+    return mode != null ? mode : SEPARATE;
   }
 }
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/model/FeatureStructures.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/model/FeatureStructures.java
new file mode 100644
index 0000000..109050d
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/model/FeatureStructures.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.model;
+
+import static java.util.stream.Collectors.toList;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.uima.cas.FeatureStructure;
+
+public class FeatureStructures implements Iterable<FeatureStructure> {
+  private final List<FeatureStructure> featureStructures;
+
+  public FeatureStructures(Collection<FeatureStructure> aFeatureStructures) {
+    featureStructures = aFeatureStructures.stream() //
+            .sorted(Comparator.comparing(fs -> {
+              return fs.getType().getName();
+            })) //
+            .collect(toList());
+  }
+
+  @Override
+  public Iterator<FeatureStructure> iterator() {
+    return featureStructures.iterator();
+  }
+
+  public boolean isEmpty() {
+    return featureStructures.isEmpty();
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/model/Header.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/model/Header.java
new file mode 100644
index 0000000..21a55a7
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/model/Header.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.model;
+
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.HEADER_OFFSET_ENCODING;
+
+import org.apache.uima.json.jsoncas2.mode.OffsetConversionMode;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.DatabindContext;
+
+public class Header {
+  @JsonProperty(HEADER_OFFSET_ENCODING)
+  private OffsetConversionMode offsetEncoding;
+
+  public Header() {
+    // Used for deserialization
+  }
+
+  public Header(DatabindContext aProvider) {
+    offsetEncoding = OffsetConversionMode.get(aProvider);
+  }
+
+  public OffsetConversionMode getOffsetEncoding() {
+    return offsetEncoding;
+  }
+
+  public void setOffsetEncoding(OffsetConversionMode aOffsetEncoding) {
+    offsetEncoding = aOffsetEncoding;
+  }
+
+  public boolean requiresSerialization() {
+    return offsetEncoding != null && offsetEncoding != OffsetConversionMode.getDefault();
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/model/Views.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/model/Views.java
new file mode 100644
index 0000000..ffab6a3
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/model/Views.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.model;
+
+import static java.util.Collections.sort;
+import static java.util.Comparator.comparing;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.uima.cas.CAS;
+
+public class Views implements Iterable<CAS> {
+  private final List<CAS> views;
+
+  public Views(CAS aCas) {
+    views = new ArrayList<>();
+    aCas.getViewIterator().forEachRemaining(views::add);
+    sort(views, comparing(CAS::getViewName));
+  }
+
+  @Override
+  public Iterator<CAS> iterator() {
+    return views.iterator();
+  }
+
+  public boolean isEmpty() {
+    return views.isEmpty();
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/FeatureStructureIdToViewIndex.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/FeatureStructureIdToViewIndex.java
new file mode 100644
index 0000000..537f9c8
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/FeatureStructureIdToViewIndex.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ref;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.databind.DatabindContext;
+
+public class FeatureStructureIdToViewIndex {
+  public static final String FS_VIEW_CACHE = "UIMA.FeatureStructureIdToViewIndex";
+
+  // FIXME Should use a map implementation with a primitive key!
+  private Map<Integer, Set<String>> fsIdToViewsCache;
+
+  public FeatureStructureIdToViewIndex() {
+    fsIdToViewsCache = new HashMap<>();
+  }
+
+  public Set<String> getViewsContainingFs(int aFsId) {
+    return fsIdToViewsCache.getOrDefault(aFsId, Collections.emptySet());
+  }
+
+  public void assignFsToView(int aFsId, String aView) {
+    fsIdToViewsCache.computeIfAbsent(aFsId, _fsId -> new HashSet<>()).add(aView);
+  }
+
+  public static void set(DatabindContext aProvider, FeatureStructureIdToViewIndex aRefCache) {
+    aProvider.setAttribute(FS_VIEW_CACHE, aRefCache);
+  }
+
+  public static FeatureStructureIdToViewIndex get(DatabindContext aProvider) {
+    return (FeatureStructureIdToViewIndex) aProvider.getAttribute(FS_VIEW_CACHE);
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/FeatureStructureToIdIndex.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/FeatureStructureToIdIndex.java
new file mode 100644
index 0000000..50e31bf
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/FeatureStructureToIdIndex.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ref;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.stream.Collectors;
+
+import org.apache.uima.cas.FeatureStructure;
+
+import com.fasterxml.jackson.databind.DatabindContext;
+
+public class FeatureStructureToIdIndex {
+  public static final String KEY = "UIMA.FeatureStructureToIdIndex";
+
+  private Map<FeatureStructure, Integer> fsToIdIndex;
+  private Map<Integer, FeatureStructure> idToFsIndex;
+
+  public FeatureStructureToIdIndex() {
+    idToFsIndex = new HashMap<>();
+    fsToIdIndex = new HashMap<>();
+  }
+
+  public void put(int aFsId, FeatureStructure aFs) {
+    idToFsIndex.put(aFsId, aFs);
+    fsToIdIndex.put(aFs, aFsId);
+  }
+
+  public OptionalInt get(FeatureStructure aFs) {
+    Integer id = fsToIdIndex.get(aFs);
+    return id != null ? OptionalInt.of(id) : OptionalInt.empty();
+  }
+
+  public List<Entry<Integer, FeatureStructure>> getAllFeatureStructures() {
+    return idToFsIndex.entrySet().stream() //
+            .sorted(Comparator.comparing(Entry::getKey)) //
+            .collect(Collectors.toList());
+  }
+
+  public Optional<FeatureStructure> get(int aId) {
+    return Optional.ofNullable(idToFsIndex.get(aId));
+  }
+
+  public static void set(DatabindContext aProvider, FeatureStructureToIdIndex aRefCache) {
+    aProvider.setAttribute(KEY, aRefCache);
+  }
+
+  public static FeatureStructureToIdIndex get(DatabindContext aProvider) {
+    return (FeatureStructureToIdIndex) aProvider.getAttribute(KEY);
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/FeatureStructureToViewIndex.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/FeatureStructureToViewIndex.java
new file mode 100644
index 0000000..f2ced13
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/FeatureStructureToViewIndex.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ref;
+
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.json.jsoncas2.model.FeatureStructures;
+
+import com.fasterxml.jackson.databind.DatabindContext;
+
+public class FeatureStructureToViewIndex {
+  public static final String FS_VIEW_CACHE = "UIMA.FeatureStructureToViewIndex";
+
+  private final FeatureStructures featureStructures;
+
+  private Map<FeatureStructure, Set<String>> fsToViewsCache;
+
+  public FeatureStructureToViewIndex() {
+    featureStructures = null;
+    fsToViewsCache = new IdentityHashMap<>();
+  }
+
+  public FeatureStructureToViewIndex(FeatureStructures aFeatureStructures) {
+    featureStructures = aFeatureStructures;
+  }
+
+  public Set<String> getViewsContainingFs(FeatureStructure aFS) {
+    if (fsToViewsCache == null) {
+      fsToViewsCache = new IdentityHashMap<>();
+      featureStructures.iterator().next().getCAS().getViewIterator().forEachRemaining(view -> {
+        for (FeatureStructure fs : view.select()) {
+          fsToViewsCache.computeIfAbsent(fs, _fs -> new HashSet<>()).add(view.getViewName());
+        }
+      });
+    }
+
+    return fsToViewsCache.get(aFS);
+  }
+
+  public void assignFsToView(FeatureStructure aFs, String aView) {
+    fsToViewsCache.computeIfAbsent(aFs, _fs -> new HashSet<>()).add(aView);
+  }
+
+  public static void set(DatabindContext aProvider, FeatureStructureToViewIndex aRefCache) {
+    aProvider.setAttribute(FS_VIEW_CACHE, aRefCache);
+  }
+
+  public static FeatureStructureToViewIndex get(DatabindContext aProvider) {
+    return (FeatureStructureToViewIndex) aProvider.getAttribute(FS_VIEW_CACHE);
+  }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/FullyQualifiedTypeRefGenerator.java
similarity index 60%
copy from uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
copy to uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/FullyQualifiedTypeRefGenerator.java
index 6ba69f5..e73d0a1 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/FullyQualifiedTypeRefGenerator.java
@@ -16,18 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.uima.cas.serdes;
+package org.apache.uima.json.jsoncas2.ref;
 
-import static org.junit.jupiter.api.Assumptions.assumeFalse;
+import java.util.function.Function;
 
-import java.util.regex.Pattern;
+import org.apache.uima.cas.Type;
 
-public class SerDesAssuptions {
-  public static void assumeNotKnownToFail(Runnable aScenario, String... aPatternsAndReasons) {
-    for (int i = 0; i < aPatternsAndReasons.length; i += 2) {
-      String pattern = aPatternsAndReasons[i];
-      String reason = aPatternsAndReasons[i + 1];
-      assumeFalse(Pattern.matches(pattern, aScenario.toString()), "Skipped because: " + reason);
-    }
+public class FullyQualifiedTypeRefGenerator implements Function<Type, String> {
+  @Override
+  public String apply(Type aType) {
+    return aType.getName();
   }
 }
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/ReferenceCache.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/ReferenceCache.java
new file mode 100644
index 0000000..58ac5b2
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/ReferenceCache.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ref;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.function.ToIntFunction;
+
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.cas.Type;
+
+import com.fasterxml.jackson.databind.DatabindContext;
+
+public class ReferenceCache {
+  public static final String KEY = "UIMA.ReferenceCache";
+
+  private final ToIntFunction<FeatureStructure> idRefGenerator;
+  private Map<FeatureStructure, Integer> idRefCache = new HashMap<>();
+
+  private final Function<Type, String> typeRefGenerator;
+  private Map<Type, String> typeRefCache = new HashMap<>();
+
+  private ReferenceCache(Builder builder) {
+    idRefGenerator = builder.idRefGeneratorSupplier.get();
+    typeRefGenerator = builder.typeRefGeneratorSupplier.get();
+  }
+
+  public int fsRef(FeatureStructure aFs) {
+    return idRefCache.computeIfAbsent(aFs, _fs -> idRefGenerator.applyAsInt(_fs));
+  }
+
+  public String typeRef(Type aType) {
+    return typeRefCache.computeIfAbsent(aType, typeRefGenerator);
+  }
+
+  /**
+   * Creates builder to build {@link ReferenceCache}.
+   * 
+   * @return created builder
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /**
+   * Builder to build {@link ReferenceCache}.
+   */
+  public static final class Builder {
+    private Supplier<ToIntFunction<FeatureStructure>> idRefGeneratorSupplier;
+    private Supplier<Function<Type, String>> typeRefGeneratorSupplier;
+
+    private Builder() {
+      idRefGeneratorSupplier = SequentialIdRefGenerator::new;
+      typeRefGeneratorSupplier = FullyQualifiedTypeRefGenerator::new;
+    }
+
+    public Builder withIdRefGeneratorSupplier(
+            Supplier<ToIntFunction<FeatureStructure>> idRefGeneratorSupplier) {
+      this.idRefGeneratorSupplier = idRefGeneratorSupplier;
+      return this;
+    }
+
+    public Builder withTypeRefGeneratorSupplier(
+            Supplier<Function<Type, String>> typeRefGeneratorSupplier) {
+      this.typeRefGeneratorSupplier = typeRefGeneratorSupplier;
+      return this;
+    }
+
+    public ReferenceCache build() {
+      return new ReferenceCache(this);
+    }
+  }
+
+  public static void set(DatabindContext aProvider, ReferenceCache aRefCache) {
+    aProvider.setAttribute(KEY, aRefCache);
+  }
+
+  public static ReferenceCache get(DatabindContext aProvider) {
+    return (ReferenceCache) aProvider.getAttribute(KEY);
+  }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/SequentialIdRefGenerator.java
similarity index 60%
copy from uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
copy to uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/SequentialIdRefGenerator.java
index 6ba69f5..8d9f8b8 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesAssuptions.java
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/SequentialIdRefGenerator.java
@@ -16,18 +16,22 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.uima.cas.serdes;
+package org.apache.uima.json.jsoncas2.ref;
 
-import static org.junit.jupiter.api.Assumptions.assumeFalse;
+import java.util.function.ToIntFunction;
 
-import java.util.regex.Pattern;
+import org.apache.uima.cas.FeatureStructure;
 
-public class SerDesAssuptions {
-  public static void assumeNotKnownToFail(Runnable aScenario, String... aPatternsAndReasons) {
-    for (int i = 0; i < aPatternsAndReasons.length; i += 2) {
-      String pattern = aPatternsAndReasons[i];
-      String reason = aPatternsAndReasons[i + 1];
-      assumeFalse(Pattern.matches(pattern, aScenario.toString()), "Skipped because: " + reason);
-    }
+public class SequentialIdRefGenerator implements ToIntFunction<FeatureStructure> {
+  /**
+   * IDs start at 1. 0 is reserved.
+   * 
+   * @see "org.apache.uima.cas.impl.XmiCasSerializer.XmiDocSerializer#writeNullObject()"
+   */
+  private int nextId = 1;
+
+  @Override
+  public int applyAsInt(FeatureStructure aT) {
+    return nextId++;
   }
 }
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/ShortTypeRefGenerator.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/ShortTypeRefGenerator.java
new file mode 100644
index 0000000..95ea7b1
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ref/ShortTypeRefGenerator.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ref;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.apache.uima.cas.Type;
+
+public class ShortTypeRefGenerator implements Function<Type, String> {
+  private Set<String> usedNames = new HashSet<>();
+
+  @Override
+  public String apply(Type aType) {
+    if (!usedNames.contains(aType.getShortName())) {
+      usedNames.add(aType.getShortName());
+      return aType.getShortName();
+    }
+
+    int n = 1;
+    String newName;
+    while (usedNames.contains(newName = aType.getShortName() + "-" + n)) {
+      n++;
+    }
+
+    usedNames.add(newName);
+    return newName;
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/CasDeserializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/CasDeserializer.java
new file mode 100644
index 0000000..d0411ae
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/CasDeserializer.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static com.fasterxml.jackson.core.JsonTokenId.ID_END_ARRAY;
+import static com.fasterxml.jackson.core.JsonTokenId.ID_END_OBJECT;
+import static com.fasterxml.jackson.core.JsonTokenId.ID_START_OBJECT;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.FEATURE_STRUCTURES_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.HEADER_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.TYPES_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.VIEWS_FIELD;
+
+import java.io.IOException;
+import java.util.Map.Entry;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.json.jsoncas2.mode.OffsetConversionMode;
+import org.apache.uima.json.jsoncas2.model.FeatureStructures;
+import org.apache.uima.json.jsoncas2.model.Header;
+import org.apache.uima.json.jsoncas2.model.Views;
+import org.apache.uima.json.jsoncas2.ref.FeatureStructureIdToViewIndex;
+import org.apache.uima.json.jsoncas2.ref.FeatureStructureToIdIndex;
+import org.apache.uima.resource.ResourceInitializationException;
+import org.apache.uima.resource.metadata.TypeSystemDescription;
+import org.apache.uima.util.CasCreationUtils;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+
+public class CasDeserializer extends CasDeserializer_ImplBase<CAS> {
+  private static final long serialVersionUID = -5937326876753347248L;
+
+  public CasDeserializer() {
+    super(CAS.class);
+  }
+
+  @Override
+  public CAS deserialize(JsonParser aParser, DeserializationContext aCtxt)
+          throws IOException, JsonProcessingException {
+
+    FeatureStructureIdToViewIndex.set(aCtxt, new FeatureStructureIdToViewIndex());
+    FeatureStructureToIdIndex.set(aCtxt, new FeatureStructureToIdIndex());
+
+    boolean isFirst = true;
+    CAS cas = null;
+    TypeSystemDescription types = null;
+
+    while (aParser.currentToken() != null) {
+      if (isFirst) {
+        isFirst = false;
+        switch (aParser.currentTokenId()) {
+          // case ID_STRING:
+          // readDocumentText(aCas, parser.getValueAsString());
+          // return;
+          // case ID_START_ARRAY:
+          // jfses = readFeatureStructuresAsArray(parser);
+          // return;
+          case ID_START_OBJECT:
+            // In this case, we continue in the main loop and process the type system and
+            // feature structures if we find them.
+            aParser.nextFieldName();
+            break;
+          default:
+            throw new IOException("JSON must start with an object, array or string value, but was ["
+                    + aParser.currentTokenId() + "]");
+        }
+      }
+
+      if (aParser.currentTokenId() == ID_END_ARRAY || aParser.currentTokenId() == ID_END_OBJECT) {
+        break;
+      }
+
+      // If we get here, we are operating on an object-type representation of the full CAS
+      switch (aParser.getCurrentName()) {
+        case HEADER_FIELD: {
+          aParser.nextValue();
+          Header header = aCtxt.readValue(aParser, Header.class);
+          OffsetConversionMode.set(aCtxt, header.getOffsetEncoding());
+          aParser.nextToken();
+          break;
+        }
+        case TYPES_FIELD:
+          aParser.nextValue();
+          types = aCtxt.readValue(aParser, TypeSystemDescription.class);
+          aParser.nextValue();
+          cas = createCasOrGetFromContext(aCtxt, types);
+          break;
+        case VIEWS_FIELD:
+          aCtxt.readValue(aParser, Views.class);
+          break;
+        case FEATURE_STRUCTURES_FIELD:
+          aCtxt.readValue(aParser, FeatureStructures.class);
+          break;
+      }
+    }
+
+    // Index FS in the respective views
+    FeatureStructureIdToViewIndex fsIdToViewIndex = FeatureStructureIdToViewIndex.get(aCtxt);
+    for (Entry<Integer, FeatureStructure> fsEntry : FeatureStructureToIdIndex.get(aCtxt)
+            .getAllFeatureStructures()) {
+      for (String viewName : fsIdToViewIndex.getViewsContainingFs(fsEntry.getKey())) {
+        cas.getView(viewName).addFsToIndexes(fsEntry.getValue());
+      }
+    }
+
+    return cas;
+  }
+
+  private CAS createCasOrGetFromContext(DeserializationContext aCtxt, TypeSystemDescription aTypes)
+          throws IOException {
+    CAS cas = getCas(aCtxt);
+    if (cas != null) {
+      return cas;
+    }
+
+    try {
+      return CasCreationUtils.createCas();
+    } catch (ResourceInitializationException e) {
+      throw new IOException(e);
+    }
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/CasDeserializer_ImplBase.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/CasDeserializer_ImplBase.java
new file mode 100644
index 0000000..fa7f908
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/CasDeserializer_ImplBase.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.CASRuntimeException;
+
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+
+public abstract class CasDeserializer_ImplBase<T> extends StdDeserializer<T> {
+
+  private static final long serialVersionUID = -1269025453230384321L;
+
+  public static final String CONTEXT_CAS = "UIMA.CAS";
+  public static final String CONTEXT_POST_PROCESSORS = "UIMA.PostProcessors";
+  public static final String CONTEXT_DOCUMENT_ANNOTATION_READ_FLAG = "UIMA.DocumentAnnotatonRead";
+
+  protected CasDeserializer_ImplBase(Class<T> aVc) {
+    super(aVc);
+  }
+
+  protected CAS getCas(DeserializationContext aCtxt) {
+    return (CAS) aCtxt.getAttribute(CONTEXT_CAS);
+  }
+
+  protected void schedulePostprocessing(DeserializationContext aCtxt, Runnable aAction) {
+    List<Runnable> postProcessors = (List<Runnable>) aCtxt.getAttribute(CONTEXT_POST_PROCESSORS);
+    if (postProcessors == null) {
+      postProcessors = new ArrayList<>();
+      aCtxt.setAttribute(CONTEXT_POST_PROCESSORS, postProcessors);
+    }
+
+    postProcessors.add(aAction);
+  }
+
+  protected void runPostprocessors(DeserializationContext aCtxt) {
+    List<Runnable> postProcessors = (List<Runnable>) aCtxt.getAttribute(CONTEXT_POST_PROCESSORS);
+    if (postProcessors != null) {
+      postProcessors.forEach(Runnable::run);
+    }
+  }
+
+  protected void markDocumentAnnotationCreated(DeserializationContext aCtxt, String aView) {
+    documentAnnotationCreatedFlags(aCtxt).add(aView);
+  }
+
+  protected boolean isDocumentAnnotationCreated(DeserializationContext aCtxt, String aView) {
+    return documentAnnotationCreatedFlags(aCtxt).contains(aView);
+  }
+
+  private Set<String> documentAnnotationCreatedFlags(DeserializationContext aCtxt) {
+    Set<String> flags = (Set<String>) aCtxt.getAttribute(CONTEXT_DOCUMENT_ANNOTATION_READ_FLAG);
+    if (flags == null) {
+      flags = new HashSet<>();
+      aCtxt.setAttribute(CONTEXT_DOCUMENT_ANNOTATION_READ_FLAG, flags);
+    }
+    return flags;
+  }
+
+  protected CAS createOrGetView(CAS aCas, String aViewName) {
+    try {
+      return aCas.getView(aViewName);
+    } catch (CASRuntimeException e) {
+      return aCas.createView(aViewName);
+    }
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/CasSerializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/CasSerializer.java
new file mode 100644
index 0000000..a5ebb10
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/CasSerializer.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.FEATURE_STRUCTURES_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.HEADER_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.TYPES_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.VIEWS_FIELD;
+
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.cas.impl.CASImpl;
+import org.apache.uima.json.jsoncas2.mode.OffsetConversionMode;
+import org.apache.uima.json.jsoncas2.mode.TypeSystemMode;
+import org.apache.uima.json.jsoncas2.model.FeatureStructures;
+import org.apache.uima.json.jsoncas2.model.Header;
+import org.apache.uima.json.jsoncas2.model.Views;
+import org.apache.uima.json.jsoncas2.ref.FeatureStructureToViewIndex;
+import org.apache.uima.json.jsoncas2.ref.ReferenceCache;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+public class CasSerializer extends StdSerializer<CAS> {
+  private static final long serialVersionUID = 6779774576723692343L;
+
+  private final Supplier<ReferenceCache> refCacheSupplier;
+
+  public CasSerializer() {
+    this(ReferenceCache.builder()::build);
+  }
+
+  public CasSerializer(Supplier<ReferenceCache> aRefCacheSupplier) {
+    super(CAS.class);
+    refCacheSupplier = aRefCacheSupplier;
+  }
+
+  @Override
+  public void serialize(CAS aCas, JsonGenerator aJg, SerializerProvider aProvider)
+          throws IOException {
+    ReferenceCache.set(aProvider, refCacheSupplier.get());
+
+    initOffsetConversion(aCas, aProvider);
+
+    aJg.writeStartObject(aCas);
+
+    serializeHeader(aCas, aJg, aProvider);
+
+    serializeTypes(aCas, aJg, aProvider);
+
+    serializeFeatureStructures(aCas, aJg, aProvider);
+
+    serializeViews(aCas, aJg, aProvider);
+
+    aJg.writeEndObject();
+  }
+
+  private void serializeHeader(CAS aCas, JsonGenerator aJg, SerializerProvider aProvider)
+          throws IOException {
+    Header header = new Header(aProvider);
+    if (header.requiresSerialization()) {
+      aJg.writeFieldName(HEADER_FIELD);
+      aProvider.defaultSerializeValue(header, aJg);
+    }
+  }
+
+  private void serializeTypes(CAS aCas, JsonGenerator aJg, SerializerProvider aProvider)
+          throws IOException {
+    if (TypeSystemMode.get(aProvider) != TypeSystemMode.NONE) {
+      aJg.writeFieldName(TYPES_FIELD);
+      aProvider.defaultSerializeValue(aCas.getTypeSystem(), aJg);
+    }
+  }
+
+  private void serializeFeatureStructures(CAS aCas, JsonGenerator aJg, SerializerProvider aProvider)
+          throws IOException {
+    FeatureStructures allFSes = findAllFeatureStructures(aCas);
+    FeatureStructureToViewIndex.set(aProvider, new FeatureStructureToViewIndex(allFSes));
+    if (!allFSes.isEmpty()) {
+      aJg.writeFieldName(FEATURE_STRUCTURES_FIELD);
+      aProvider.defaultSerializeValue(allFSes, aJg);
+    }
+  }
+
+  private void serializeViews(CAS aCas, JsonGenerator aJg, SerializerProvider aProvider)
+          throws IOException {
+    Views views = new Views(aCas);
+    if (!views.isEmpty()) {
+      aJg.writeFieldName(VIEWS_FIELD);
+      aProvider.defaultSerializeValue(views, aJg);
+    }
+  }
+
+  private void initOffsetConversion(CAS aCas, SerializerProvider aProvider) {
+    for (CAS view : new Views(aCas)) {
+      OffsetConversionMode.initConverter(aProvider, view.getViewName(), view.getDocumentText());
+    }
+  }
+
+  private FeatureStructures findAllFeatureStructures(CAS aCas) {
+    Set<FeatureStructure> allFSes = new LinkedHashSet<>();
+    ((CASImpl) aCas).walkReachablePlusFSsSorted(allFSes::add, null, null, null);
+    return new FeatureStructures(allFSes);
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/CommonArrayFSSerializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/CommonArrayFSSerializer.java
new file mode 100644
index 0000000..03980e4
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/CommonArrayFSSerializer.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.ELEMENTS_FIELD;
+
+import java.io.IOException;
+
+import org.apache.uima.cas.BooleanArrayFS;
+import org.apache.uima.cas.ByteArrayFS;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.CommonArrayFS;
+import org.apache.uima.cas.DoubleArrayFS;
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.cas.FloatArrayFS;
+import org.apache.uima.cas.IntArrayFS;
+import org.apache.uima.cas.LongArrayFS;
+import org.apache.uima.cas.ShortArrayFS;
+import org.apache.uima.cas.StringArrayFS;
+import org.apache.uima.jcas.cas.FSArray;
+import org.apache.uima.json.jsoncas2.ref.ReferenceCache;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+@SuppressWarnings("rawtypes")
+public class CommonArrayFSSerializer extends FeatureStructureSerializer_ImplBase<CommonArrayFS> {
+  private static final long serialVersionUID = 4842019532480552884L;
+
+  public CommonArrayFSSerializer() {
+    super(CommonArrayFS.class);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  protected void writeBody(SerializerProvider aProvider, JsonGenerator aJg, FeatureStructure aFs)
+          throws IOException {
+    aJg.writeFieldName(ELEMENTS_FIELD);
+    switch (aFs.getType().getName()) {
+      case CAS.TYPE_NAME_BOOLEAN_ARRAY: {
+        aJg.writeStartArray();
+        boolean[] values = ((BooleanArrayFS) aFs).toArray();
+        for (int i = 0; i < values.length; i++) {
+          aJg.writeBoolean(values[i]);
+        }
+        aJg.writeEndArray();
+        break;
+      }
+      case CAS.TYPE_NAME_BYTE_ARRAY: {
+        aJg.writeBinary(((ByteArrayFS) aFs).toArray());
+        break;
+      }
+      case CAS.TYPE_NAME_DOUBLE_ARRAY: {
+        double[] values = ((DoubleArrayFS) aFs).toArray();
+        aJg.writeArray(values, 0, values.length);
+        break;
+      }
+      case CAS.TYPE_NAME_FLOAT_ARRAY: {
+        float[] fValues = ((FloatArrayFS) aFs).toArray();
+        double[] dValues = new double[fValues.length];
+        for (int i = 0; i < fValues.length; i++) {
+          dValues[i] = fValues[i];
+        }
+        aJg.writeArray(dValues, 0, dValues.length);
+        break;
+      }
+      case CAS.TYPE_NAME_INTEGER_ARRAY: {
+        int[] values = ((IntArrayFS) aFs).toArray();
+        aJg.writeArray(values, 0, values.length);
+        break;
+      }
+      case CAS.TYPE_NAME_LONG_ARRAY: {
+        long[] values = ((LongArrayFS) aFs).toArray();
+        aJg.writeArray(values, 0, values.length);
+        break;
+      }
+      case CAS.TYPE_NAME_SHORT_ARRAY: {
+        short[] sValues = ((ShortArrayFS) aFs).toArray();
+        int[] iValues = new int[sValues.length];
+        for (int i = 0; i < sValues.length; i++) {
+          iValues[i] = sValues[i];
+        }
+        aJg.writeArray(iValues, 0, iValues.length);
+        break;
+      }
+      case CAS.TYPE_NAME_STRING_ARRAY: {
+        String[] values = ((StringArrayFS) aFs).toArray();
+        aJg.writeArray(values, 0, values.length);
+        break;
+      }
+      case CAS.TYPE_NAME_FS_ARRAY: // fall-through
+      default: {
+        aJg.writeStartArray();
+        ReferenceCache refCache = ReferenceCache.get(aProvider);
+        for (FeatureStructure fs : ((FSArray<FeatureStructure>) aFs)) {
+          aJg.writeNumber(refCache.fsRef(fs));
+        }
+        aJg.writeEndArray();
+        break;
+      }
+    }
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureDeserializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureDeserializer.java
new file mode 100644
index 0000000..b615149
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureDeserializer.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.ARRAY_SUFFIX;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.ELEMENT_TYPE_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.NAME_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.RANGE_FIELD;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import org.apache.uima.UIMAFramework;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.resource.metadata.FeatureDescription;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class FeatureDeserializer extends CasDeserializer_ImplBase<FeatureDescription> {
+  private static final long serialVersionUID = 1L;
+
+  public FeatureDeserializer() {
+    super(FeatureDescription.class);
+  }
+
+  @Override
+  public FeatureDescription deserialize(JsonParser aParser, DeserializationContext aCtxt)
+          throws IOException, JsonProcessingException {
+    JsonNode node = aParser.readValueAsTree();
+
+    if (!node.isObject()) {
+      throw new JsonParseException(aParser, "Feature declaration must be a JSON object");
+    }
+
+    String featureName = node.get(NAME_FIELD).asText();
+    String featureRangeType = node.get(RANGE_FIELD).asText();
+    Optional<String> componentType;
+    if (featureRangeType.endsWith(ARRAY_SUFFIX)) {
+      componentType = Optional
+              .of(featureRangeType.substring(0, featureRangeType.length() - ARRAY_SUFFIX.length()));
+      featureRangeType = CAS.TYPE_NAME_FS_ARRAY;
+    } else {
+      componentType = Optional.ofNullable(node.get(ELEMENT_TYPE_FIELD)).map(JsonNode::asText);
+    }
+
+    FeatureDescription fd = UIMAFramework.getResourceSpecifierFactory().createFeatureDescription();
+    fd.setName(featureName);
+    fd.setRangeTypeName(featureRangeType);
+    fd.setElementType(componentType.orElse(null));
+    // fd.setMultipleReferencesAllowed();
+    // fd.setDescription("");
+    // td.setSourceUrl(aParser.getTokenLocation().sourceDescription());
+    return fd;
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureSerializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureSerializer.java
new file mode 100644
index 0000000..3df1159
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureSerializer.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static org.apache.uima.cas.CAS.TYPE_NAME_FS_ARRAY;
+import static org.apache.uima.cas.CAS.TYPE_NAME_TOP;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.ARRAY_SUFFIX;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.ELEMENT_TYPE_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.NAME_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.RANGE_FIELD;
+import static org.apache.uima.json.jsoncas2.mode.ArrayTypeMode.AS_ARRAY_TYPED_RANGE;
+
+import java.io.IOException;
+
+import org.apache.uima.cas.Feature;
+import org.apache.uima.cas.Type;
+import org.apache.uima.json.jsoncas2.mode.ArrayTypeMode;
+import org.apache.uima.json.jsoncas2.ref.ReferenceCache;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+public class FeatureSerializer extends StdSerializer<Feature> {
+  private static final long serialVersionUID = 6706550270922386356L;
+
+  private ArrayTypeMode arrayMode = AS_ARRAY_TYPED_RANGE;
+
+  public FeatureSerializer() {
+    super(Feature.class);
+  }
+
+  public void setArrayMode(ArrayTypeMode aArrayMode) {
+    arrayMode = aArrayMode;
+  }
+
+  public ArrayTypeMode getArrayMode() {
+    return arrayMode;
+  }
+
+  @Override
+  public void serialize(Feature aFeature, JsonGenerator jg, SerializerProvider aProvider)
+          throws IOException {
+    ReferenceCache refCache = ReferenceCache.get(aProvider);
+
+    jg.writeStartObject(aFeature);
+
+    jg.writeStringField(NAME_FIELD, aFeature.getShortName());
+
+    // special check for array range types, which are represented in the CAS as
+    // elementType[] but in the descriptor as an FSArray with an <elementType>
+    Type rangeType = aFeature.getRange();
+    if (rangeType.isArray() && !rangeType.getComponentType().isPrimitive()) {
+      switch (arrayMode) {
+        case AS_ARRAY_TYPED_RANGE:
+          serializeArrayFieldAsRangeWithArrayMarker(refCache, jg, aFeature);
+          break;
+        case AS_RANGE_AND_ELEMENT:
+          serializeArrayFieldAsTypeAndElementType(refCache, jg, aFeature);
+          break;
+      }
+    } else {
+      serializeField(refCache, jg, aFeature);
+    }
+
+    jg.writeEndObject();
+  }
+
+  private void serializeField(ReferenceCache refCache, JsonGenerator jg, Feature aFeature)
+          throws IOException {
+    jg.writeStringField(RANGE_FIELD, refCache.typeRef(aFeature.getRange()));
+    Type componentType = aFeature.getRange().getComponentType();
+    if (componentType != null) {
+      jg.writeStringField(ELEMENT_TYPE_FIELD, refCache.typeRef(componentType));
+    }
+  }
+
+  /**
+   * @see ArrayTypeMode#AS_ARRAY_TYPED_RANGE
+   */
+  private void serializeArrayFieldAsRangeWithArrayMarker(ReferenceCache refCache, JsonGenerator jg,
+          Feature aFeature) throws IOException {
+    jg.writeStringField(RANGE_FIELD,
+            refCache.typeRef(aFeature.getRange().getComponentType()) + ARRAY_SUFFIX);
+  }
+
+  /**
+   * @see ArrayTypeMode#AS_RANGE_AND_ELEMENT
+   */
+  private void serializeArrayFieldAsTypeAndElementType(ReferenceCache refCache, JsonGenerator jg,
+          Feature aFeature) throws IOException {
+    Type rangeType = aFeature.getRange();
+    jg.writeStringField(RANGE_FIELD, TYPE_NAME_FS_ARRAY);
+
+    // Component type can be omitted if it is the default (TOP)
+    if (!TYPE_NAME_TOP.equals(rangeType.getComponentType().getName())) {
+      jg.writeStringField(ELEMENT_TYPE_FIELD, refCache.typeRef(rangeType.getComponentType()));
+    }
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructureDeserializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructureDeserializer.java
new file mode 100644
index 0000000..494d67e
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructureDeserializer.java
@@ -0,0 +1,658 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static com.fasterxml.jackson.core.JsonToken.END_OBJECT;
+import static com.fasterxml.jackson.core.JsonToken.START_OBJECT;
+import static java.lang.Integer.MIN_VALUE;
+import static org.apache.uima.cas.CAS.FEATURE_BASE_NAME_SOFAARRAY;
+import static org.apache.uima.cas.CAS.FEATURE_BASE_NAME_SOFAID;
+import static org.apache.uima.cas.CAS.FEATURE_BASE_NAME_SOFAMIME;
+import static org.apache.uima.cas.CAS.FEATURE_BASE_NAME_SOFANUM;
+import static org.apache.uima.cas.CAS.FEATURE_BASE_NAME_SOFASTRING;
+import static org.apache.uima.cas.CAS.FEATURE_BASE_NAME_SOFAURI;
+import static org.apache.uima.cas.CAS.TYPE_NAME_BOOLEAN_ARRAY;
+import static org.apache.uima.cas.CAS.TYPE_NAME_BYTE;
+import static org.apache.uima.cas.CAS.TYPE_NAME_BYTE_ARRAY;
+import static org.apache.uima.cas.CAS.TYPE_NAME_DOCUMENT_ANNOTATION;
+import static org.apache.uima.cas.CAS.TYPE_NAME_DOUBLE;
+import static org.apache.uima.cas.CAS.TYPE_NAME_DOUBLE_ARRAY;
+import static org.apache.uima.cas.CAS.TYPE_NAME_FLOAT;
+import static org.apache.uima.cas.CAS.TYPE_NAME_FLOAT_ARRAY;
+import static org.apache.uima.cas.CAS.TYPE_NAME_INTEGER;
+import static org.apache.uima.cas.CAS.TYPE_NAME_INTEGER_ARRAY;
+import static org.apache.uima.cas.CAS.TYPE_NAME_LONG;
+import static org.apache.uima.cas.CAS.TYPE_NAME_LONG_ARRAY;
+import static org.apache.uima.cas.CAS.TYPE_NAME_SHORT;
+import static org.apache.uima.cas.CAS.TYPE_NAME_SHORT_ARRAY;
+import static org.apache.uima.cas.CAS.TYPE_NAME_SOFA;
+import static org.apache.uima.cas.CAS.TYPE_NAME_STRING_ARRAY;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.ANCHOR_FEATURE_PREFIX;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.ID_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.NUMBER_FLOAT_NAN;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.NUMBER_FLOAT_NEGATIVE_INFINITY;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.NUMBER_FLOAT_NEGATIVE_INFINITY_ABBR;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.NUMBER_FLOAT_POSITIVE_INFINITY;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.NUMBER_FLOAT_POSITIVE_INFINITY_ABBR;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.NUMERIC_FEATURE_PREFIX;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.REF_FEATURE_PREFIX;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.RESERVED_FIELD_PREFIX;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.TYPE_FIELD;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+
+import org.apache.uima.cas.ArrayFS;
+import org.apache.uima.cas.BooleanArrayFS;
+import org.apache.uima.cas.ByteArrayFS;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.CASRuntimeException;
+import org.apache.uima.cas.DoubleArrayFS;
+import org.apache.uima.cas.Feature;
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.cas.FloatArrayFS;
+import org.apache.uima.cas.IntArrayFS;
+import org.apache.uima.cas.LongArrayFS;
+import org.apache.uima.cas.ShortArrayFS;
+import org.apache.uima.cas.SofaFS;
+import org.apache.uima.cas.StringArrayFS;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.TypeSystem;
+import org.apache.uima.cas.impl.CASImpl;
+import org.apache.uima.jcas.cas.TOP;
+import org.apache.uima.jcas.tcas.Annotation;
+import org.apache.uima.json.jsoncas2.mode.OffsetConversionMode;
+import org.apache.uima.json.jsoncas2.ref.FeatureStructureToIdIndex;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+
+public class FeatureStructureDeserializer extends CasDeserializer_ImplBase<FeatureStructure> {
+  private static final long serialVersionUID = -5937326876753347248L;
+
+  private final Logger log = LoggerFactory.getLogger(getClass());
+
+  private enum FieldType {
+    REGULAR, REFERENCE, NUMBER, ANCHOR
+  }
+
+  public FeatureStructureDeserializer() {
+    super(FeatureStructure.class);
+  }
+
+  @Override
+  public FeatureStructure deserialize(JsonParser aParser, DeserializationContext aCtxt)
+          throws IOException, JsonProcessingException {
+
+    CAS cas = getCas(aCtxt);
+
+    if (aParser.currentToken() != START_OBJECT) {
+      throw new JsonParseException(aParser, "Expected feature structure to start with "
+              + START_OBJECT + " but found " + aParser.currentToken() + " instead");
+    }
+
+    int fsId = MIN_VALUE;
+    // Handle case where feature structures section is represented as a map instead of an array
+    if (aParser.getCurrentName() != null) {
+      fsId = Integer.parseInt(aParser.getCurrentName());
+    }
+
+    FeatureStructure fs = null;
+    aParser.nextValue();
+    while (aParser.currentToken() != END_OBJECT) {
+      String fieldName = aParser.currentName();
+
+      // log.trace("Deserializing {}: {}", fieldName, aParser.getText());
+
+      if (fieldName.startsWith(RESERVED_FIELD_PREFIX)) {
+        switch (fieldName) {
+          case ID_FIELD:
+            // Handle case where feature structures section is represented as an array
+            fsId = aParser.getIntValue();
+            break;
+          case TYPE_FIELD:
+            if (fsId == MIN_VALUE) {
+              throw new JsonParseException(aParser, TYPE_FIELD + " must come after " + ID_FIELD);
+            }
+            String typeName = aParser.getValueAsString();
+
+            switch (typeName) {
+              case TYPE_NAME_BOOLEAN_ARRAY:
+                fs = deserializeBooleanArray(aParser, cas);
+                FeatureStructureToIdIndex.get(aCtxt).put(fsId, fs);
+                continue;
+              case TYPE_NAME_BYTE_ARRAY:
+                fs = deserializeByteArray(aParser, cas);
+                FeatureStructureToIdIndex.get(aCtxt).put(fsId, fs);
+                continue;
+              case TYPE_NAME_DOUBLE_ARRAY:
+                fs = deserializeDoubleArray(aParser, cas);
+                FeatureStructureToIdIndex.get(aCtxt).put(fsId, fs);
+                continue;
+              case TYPE_NAME_FLOAT_ARRAY:
+                fs = deserializeFloatArray(aParser, cas);
+                FeatureStructureToIdIndex.get(aCtxt).put(fsId, fs);
+                continue;
+              case TYPE_NAME_INTEGER_ARRAY:
+                fs = deserializeIntegerArray(aParser, cas);
+                FeatureStructureToIdIndex.get(aCtxt).put(fsId, fs);
+                continue;
+              case TYPE_NAME_LONG_ARRAY:
+                fs = deserializeLongArray(aParser, cas);
+                FeatureStructureToIdIndex.get(aCtxt).put(fsId, fs);
+                continue;
+              case TYPE_NAME_SHORT_ARRAY:
+                fs = deserializeShortArray(aParser, cas);
+                FeatureStructureToIdIndex.get(aCtxt).put(fsId, fs);
+                continue;
+              case TYPE_NAME_STRING_ARRAY:
+                fs = deserializeStringArray(aParser, cas);
+                FeatureStructureToIdIndex.get(aCtxt).put(fsId, fs);
+                continue;
+              case CAS.TYPE_NAME_FS_ARRAY:
+                fs = deserializeFsArray(aParser, cas, aCtxt);
+                FeatureStructureToIdIndex.get(aCtxt).put(fsId, fs);
+                continue;
+              case TYPE_NAME_SOFA:
+                fs = createSofaFS(cas, aParser, aCtxt);
+                FeatureStructureToIdIndex.get(aCtxt).put(fsId, fs);
+                continue;
+              default:
+                fs = createFS(aParser, aCtxt, fsId, cas);
+                break;
+            }
+            break;
+          // case FLAGS_FIELD:
+          // // FIXME: We probably don't need the flags field at all.
+          // aParser.nextToken();
+          // aParser.skipChildren();
+          // aParser.nextToken();
+          // break;
+        }
+
+        aParser.nextValue();
+        continue;
+      }
+
+      if (fs == null || fsId == MIN_VALUE) {
+        throw new JsonParseException(aParser,
+                "Features must come after " + ID_FIELD + "" + TYPE_FIELD);
+      }
+
+      FieldType fieldType = FieldType.REGULAR;
+      if (fieldName.startsWith(REF_FEATURE_PREFIX)) {
+        fieldName = fieldName.substring(REF_FEATURE_PREFIX.length());
+        fieldType = FieldType.REFERENCE;
+      } else if (fieldName.startsWith(NUMERIC_FEATURE_PREFIX)) {
+        fieldName = fieldName.substring(NUMERIC_FEATURE_PREFIX.length());
+        fieldType = FieldType.NUMBER;
+      } else if (fieldName.startsWith(ANCHOR_FEATURE_PREFIX)) {
+        fieldName = fieldName.substring(ANCHOR_FEATURE_PREFIX.length());
+        fieldType = FieldType.ANCHOR;
+      }
+
+      if (CAS.FEATURE_FULL_NAME_SOFA
+              .equals(fs.getType().getFeatureByBaseName(fieldName).getName())) {
+        // Ignore the SofA feature of AnnotationBase-derived types - this feature cannot be set
+        // manually - this happens (hopefully) when adding the AnnotationBase FS to the indexes of
+        // the particular SofA.
+        aParser.nextValue();
+        continue;
+      }
+
+      if (fieldType == FieldType.REFERENCE) {
+        deserializeFsReference(aParser, aCtxt, fs, fieldName);
+        aParser.nextValue();
+        continue;
+      }
+
+      deserializePrimitive(aParser, aCtxt, fs, fieldName, fieldType);
+      aParser.nextValue();
+    }
+
+    // Special handling of the document annotation
+    handleDocumentAnnotation(aCtxt, cas, fs);
+
+    // Register the loaded FS
+    FeatureStructureToIdIndex.get(aCtxt).put(fsId, fs);
+
+    return fs;
+  }
+
+  private FeatureStructure createFS(JsonParser aParser, DeserializationContext aCtxt, int aFsId,
+          CAS aCas) throws IOException {
+    String typeName = aParser.getValueAsString();
+    TypeSystem ts = aCas.getTypeSystem();
+    Type t = ts.getType(typeName);
+    if (t == null) {
+      throw new JsonParseException(aParser, "Type not found in type system: " + typeName);
+    }
+
+    return aCas.createFS(t);
+  }
+
+  // Case 1: there is no document annotation yet (unlikely since the document text has probably
+  // been set already and this implicitly triggers the creation of a document annotation)
+  // -> the document annotation that was created became the primary document annotation,
+  // nothing else to do
+  //
+  // Case 2: there is already a document annotation of the same type as what we deserialize
+  // 2a> if it is the first time we deserialize a document annotation, then fill it
+  // 2b> otherwise add the new document annotation
+  //
+  // Case 3: there is already a document annotation but is has a different type
+  // 3a> if it is the first time we deserialize a document annotation, then replace it
+  // 3b> otherwise add the new document annotation
+  private void handleDocumentAnnotation(DeserializationContext aCtxt, CAS aCas,
+          FeatureStructure aFS) throws JsonParseException {
+    TypeSystem ts = aCas.getTypeSystem();
+    Type docAnnoType = ts.getType(TYPE_NAME_DOCUMENT_ANNOTATION);
+    if (!ts.subsumes(docAnnoType, aFS.getType())) {
+      return;
+    }
+
+    String viewName = ((Annotation) aFS).getSofa().getSofaID();
+
+    // Case 2b/3b: we already have handled the primary document annotation, this one is an extra
+    if (isDocumentAnnotationCreated(aCtxt, viewName)) {
+      // Nothing extra to do
+      return;
+    }
+
+    Collection<TOP> docAnnotations = aCas.getIndexedFSs(docAnnoType);
+    // Case 1: was no document annotation yet
+    if (docAnnotations.isEmpty()) {
+      markDocumentAnnotationCreated(aCtxt, viewName);
+      return;
+    }
+
+    // Case 3a: need to replace the existing document annotation because it has a different type
+    docAnnotations.forEach(aCas::removeFsFromIndexes);
+    aCas.addFsToIndexes(aFS);
+    markDocumentAnnotationCreated(aCtxt, viewName);
+  }
+
+  /**
+   * Parses SofaFS entries from the feature structure list. If no data has been set in the SofaFS,
+   * this method will return null.
+   */
+  private FeatureStructure createSofaFS(CAS aCas, JsonParser aParser, DeserializationContext aCtxt)
+          throws IOException {
+    int sofaNum = -1;
+    String sofaID = null;
+    String mimeType = null;
+    String sofaURI = null;
+    String sofaString = null;
+    FeatureStructure sofaArray = null;
+
+    aParser.nextValue();
+    while (aParser.currentToken() != JsonToken.END_OBJECT) {
+      String fieldName = aParser.currentName();
+
+      switch (fieldName) {
+        case FEATURE_BASE_NAME_SOFANUM:
+          sofaNum = aParser.getValueAsInt();
+          break;
+        case FEATURE_BASE_NAME_SOFAID:
+          sofaID = aParser.getValueAsString();
+          break;
+        case FEATURE_BASE_NAME_SOFAMIME:
+          mimeType = aParser.getValueAsString();
+          break;
+        case FEATURE_BASE_NAME_SOFAURI:
+          sofaURI = aParser.getValueAsString();
+          break;
+        case FEATURE_BASE_NAME_SOFASTRING:
+          sofaString = aParser.getValueAsString();
+          break;
+        case REF_FEATURE_PREFIX + FEATURE_BASE_NAME_SOFAARRAY: {
+          FeatureStructureToIdIndex fsIdx = FeatureStructureToIdIndex.get(aCtxt);
+          int sofaArrayId = aParser.getValueAsInt();
+          sofaArray = fsIdx.get(sofaArrayId)
+                  .orElseThrow(() -> new JsonParseException(aParser, "The SofA array with ID "
+                          + sofaArrayId + " must come before the SofAFS referencing it."));
+          break;
+        }
+        default:
+          throw new JsonParseException(aParser, "Unexpeced field in SofA: " + fieldName);
+      }
+
+      aParser.nextValue();
+    }
+
+    if (sofaID == null) {
+      throw new JsonParseException(aParser, "SofA must have a sofaID");
+    }
+
+    CAS view = createOrGetView(aCas, sofaID);
+
+    if (sofaURI != null) {
+      view.setSofaDataURI(sofaURI, mimeType);
+    } else if (sofaString != null) {
+      view.setSofaDataString(sofaString, mimeType);
+      OffsetConversionMode.initConverter(aCtxt, sofaID, sofaString);
+    } else if (sofaArray != null) {
+      view.setSofaDataArray(sofaArray, mimeType);
+    }
+
+    SofaFS sofa = view.getSofa();
+    if (sofa != null) {
+      return sofa;
+    }
+
+    return ((CASImpl) view).getSofaRef();
+  }
+
+  private BooleanArrayFS deserializeBooleanArray(JsonParser aParser, CAS aCas) throws IOException {
+    // Skip array opening and go to first value (or end of array if there is no value)
+    aParser.nextValue();
+    aParser.nextValue();
+    List<Boolean> values = new ArrayList<>();
+    while (aParser.currentToken() != JsonToken.END_ARRAY) {
+      values.add(aParser.getBooleanValue());
+      aParser.nextValue();
+    }
+    BooleanArrayFS arrayFs = aCas.createBooleanArrayFS(values.size());
+    for (int i = 0; i < values.size(); i++) {
+      arrayFs.set(i, values.get(i));
+    }
+    return arrayFs;
+  }
+
+  private ByteArrayFS deserializeByteArray(JsonParser aParser, CAS aCas) throws IOException {
+    aParser.nextValue();
+    byte[] bytes = aParser.getBinaryValue();
+    ByteArrayFS arrayFs = aCas.createByteArrayFS(bytes.length);
+    arrayFs.copyFromArray(bytes, 0, 0, bytes.length);
+    aParser.nextToken();
+    return arrayFs;
+  }
+
+  private DoubleArrayFS deserializeDoubleArray(JsonParser aParser, CAS aCas) throws IOException {
+    // Skip array opening and go to first value (or end of array if there is no value)
+    aParser.nextValue();
+    aParser.nextValue();
+    List<Double> values = new ArrayList<>();
+    while (aParser.currentToken() != JsonToken.END_ARRAY) {
+      values.add(readDoubleValue(aParser));
+      aParser.nextValue();
+    }
+    DoubleArrayFS arrayFs = aCas.createDoubleArrayFS(values.size());
+    for (int i = 0; i < values.size(); i++) {
+      arrayFs.set(i, values.get(i));
+    }
+    return arrayFs;
+  }
+
+  private FloatArrayFS deserializeFloatArray(JsonParser aParser, CAS aCas) throws IOException {
+    // Skip array opening and go to first value (or end of array if there is no value)
+    aParser.nextValue();
+    aParser.nextValue();
+    List<Float> values = new ArrayList<>();
+    while (aParser.currentToken() != JsonToken.END_ARRAY) {
+      values.add((float) readDoubleValue(aParser));
+      aParser.nextValue();
+    }
+    FloatArrayFS arrayFs = aCas.createFloatArrayFS(values.size());
+    for (int i = 0; i < values.size(); i++) {
+      arrayFs.set(i, values.get(i));
+    }
+    return arrayFs;
+  }
+
+  private double readDoubleValue(JsonParser aParser) throws IOException {
+    if (aParser.currentToken() == JsonToken.VALUE_NUMBER_FLOAT) {
+      return aParser.getDoubleValue();
+    }
+
+    if (aParser.currentToken() == JsonToken.VALUE_STRING) {
+      switch (aParser.getValueAsString()) {
+        case NUMBER_FLOAT_NAN:
+          return Double.NaN;
+        case NUMBER_FLOAT_POSITIVE_INFINITY:
+        case NUMBER_FLOAT_POSITIVE_INFINITY_ABBR:
+          return Double.POSITIVE_INFINITY;
+        case NUMBER_FLOAT_NEGATIVE_INFINITY:
+        case NUMBER_FLOAT_NEGATIVE_INFINITY_ABBR:
+          return Double.NEGATIVE_INFINITY;
+        default:
+          throw new JsonParseException(aParser,
+                  "Expected special floating point value (NaN, -Inf, -Infinity, Inf, Infinity), "
+                          + "but got [" + aParser.getValueAsString() + "]");
+      }
+    }
+
+    throw new JsonParseException(aParser,
+            "Expected floating point value as VALUE_NUMBER_FLOAT or VALUE_STRING for special "
+                    + "values (Nan, -Infinity, +Infinity), but got [" + aParser.currentToken()
+                    + "]");
+  }
+
+  private IntArrayFS deserializeIntegerArray(JsonParser aParser, CAS aCas) throws IOException {
+    // Skip array opening and go to first value (or end of array if there is no value)
+    aParser.nextValue();
+    aParser.nextValue();
+    List<Integer> values = new ArrayList<>();
+    while (aParser.currentToken() != JsonToken.END_ARRAY) {
+      values.add(aParser.getIntValue());
+      aParser.nextValue();
+    }
+    IntArrayFS arrayFs = aCas.createIntArrayFS(values.size());
+    for (int i = 0; i < values.size(); i++) {
+      arrayFs.set(i, values.get(i));
+    }
+    return arrayFs;
+  }
+
+  private LongArrayFS deserializeLongArray(JsonParser aParser, CAS aCas) throws IOException {
+    // Skip array opening and go to first value (or end of array if there is no value)
+    aParser.nextValue();
+    aParser.nextValue();
+    List<Long> values = new ArrayList<>();
+    while (aParser.currentToken() != JsonToken.END_ARRAY) {
+      values.add(aParser.getLongValue());
+      aParser.nextValue();
+    }
+    LongArrayFS arrayFs = aCas.createLongArrayFS(values.size());
+    for (int i = 0; i < values.size(); i++) {
+      arrayFs.set(i, values.get(i));
+    }
+    return arrayFs;
+  }
+
+  private ShortArrayFS deserializeShortArray(JsonParser aParser, CAS aCas) throws IOException {
+    // Skip array opening and go to first value (or end of array if there is no value)
+    aParser.nextValue();
+    aParser.nextValue();
+    List<Short> values = new ArrayList<>();
+    while (aParser.currentToken() != JsonToken.END_ARRAY) {
+      values.add((short) aParser.getIntValue());
+      aParser.nextValue();
+    }
+    ShortArrayFS arrayFs = aCas.createShortArrayFS(values.size());
+    for (int i = 0; i < values.size(); i++) {
+      arrayFs.set(i, values.get(i));
+    }
+    return arrayFs;
+  }
+
+  private StringArrayFS deserializeStringArray(JsonParser aParser, CAS aCas) throws IOException {
+    // Go to array opening
+    aParser.nextValue();
+    // Go to first value if any or to end of array
+    aParser.nextValue();
+    List<String> values = new ArrayList<>();
+    while (aParser.currentToken() != JsonToken.END_ARRAY) {
+      values.add(aParser.getValueAsString());
+      aParser.nextValue();
+    }
+    StringArrayFS arrayFs = aCas.createStringArrayFS(values.size());
+    for (int i = 0; i < values.size(); i++) {
+      arrayFs.set(i, values.get(i));
+    }
+    return arrayFs;
+  }
+
+  private ArrayFS<FeatureStructure> deserializeFsArray(JsonParser aParser, CAS aCas,
+          DeserializationContext aCtxt) throws IOException {
+    // Go to array opening
+    aParser.nextValue();
+    // Go to first value if any or to end of array
+    aParser.nextValue();
+    List<Integer> values = new ArrayList<>();
+    while (aParser.currentToken() != JsonToken.END_ARRAY) {
+      values.add(aParser.getIntValue());
+      aParser.nextValue();
+    }
+
+    @SuppressWarnings("unchecked")
+    ArrayFS<FeatureStructure> arrayFs = aCas.createArrayFS(values.size());
+    FeatureStructureToIdIndex idToFsIdx = FeatureStructureToIdIndex.get(aCtxt);
+    for (int i = 0; i < values.size(); i++) {
+      int targetFsId = values.get(i);
+      Optional<FeatureStructure> targetFs = idToFsIdx.get(targetFsId);
+      if (targetFs.isPresent()) {
+        arrayFs.set(i, targetFs.get());
+      } else {
+        int finalIndex = i;
+        schedulePostprocessing(aCtxt, () -> {
+          arrayFs.set(finalIndex,
+                  idToFsIdx.get(targetFsId)
+                          .orElseThrow(() -> new NoSuchElementException("Unable to resolve ID ["
+                                  + targetFsId + "] during array post-processing")));
+        });
+      }
+    }
+    return arrayFs;
+  }
+
+  private void deserializePrimitive(JsonParser aParser, DeserializationContext aCtxt,
+          FeatureStructure aFs, String aFeatureName, FieldType fieldType)
+          throws CASRuntimeException, IOException {
+    Feature feature = aFs.getType().getFeatureByBaseName(aFeatureName);
+
+    if (fieldType == FieldType.NUMBER) {
+      deserializeFloatingPointValue(aParser, aFs, feature);
+      return;
+    }
+
+    switch (aParser.currentToken()) {
+      case VALUE_NULL:
+        // No need do to anything really - we just leave the feature alone
+        break;
+      case VALUE_TRUE: // fall-through
+      case VALUE_FALSE:
+        aFs.setBooleanValue(feature, aParser.getBooleanValue());
+        break;
+      case VALUE_STRING:
+        aFs.setStringValue(feature, aParser.getValueAsString());
+        break;
+      case VALUE_NUMBER_FLOAT: // JSON does not distinguish between double and float
+        deserializeFloatingPointValue(aParser, aFs, feature);
+        break;
+      case VALUE_NUMBER_INT:
+        deserializeIntegerValue(aParser, aCtxt, aFs, feature, fieldType);
+        break;
+      default:
+        throw new JsonParseException(aParser,
+                "Expected a feature value as null, a boolean, string, or number but got "
+                        + aParser.currentToken());
+    }
+  }
+
+  private void deserializeFsReference(JsonParser aParser, DeserializationContext aCtxt,
+          FeatureStructure aFs, String aFieldName) throws IOException {
+    FeatureStructureToIdIndex idToFsIdx = FeatureStructureToIdIndex.get(aCtxt);
+    int targetFsId = aParser.getIntValue();
+    Optional<FeatureStructure> targetFs = idToFsIdx.get(targetFsId);
+    Feature feature = aFs.getType().getFeatureByBaseName(aFieldName);
+    if (targetFs.isPresent()) {
+      aFs.setFeatureValue(feature, targetFs.get());
+    } else {
+      FeatureStructure finalFs = aFs;
+      schedulePostprocessing(aCtxt, () -> {
+        finalFs.setFeatureValue(feature,
+                idToFsIdx.get(targetFsId).orElseThrow(() -> new NoSuchElementException(
+                        "Unable to resolve ID [" + targetFsId + "] during post-processing")));
+      });
+    }
+  }
+
+  private void deserializeFloatingPointValue(JsonParser aParser, FeatureStructure aFs,
+          Feature aFeature) throws CASRuntimeException, IOException {
+    switch (aFeature.getRange().getName()) {
+      case TYPE_NAME_DOUBLE:
+        aFs.setDoubleValue(aFeature, readDoubleValue(aParser));
+        break;
+      case TYPE_NAME_FLOAT:
+        aFs.setFloatValue(aFeature, (float) readDoubleValue(aParser));
+        break;
+      default:
+        throw new JsonParseException(aParser, "Feature of type " + aFeature.getRange().getName()
+                + " cannot be set from a JSON value of type " + aParser.currentToken());
+    }
+  }
+
+  private void deserializeIntegerValue(JsonParser aParser, DeserializationContext aCtxt,
+          FeatureStructure aFs, Feature aFeature, FieldType fieldType)
+          throws CASRuntimeException, IOException {
+    switch (aFeature.getRange().getName()) {
+      case TYPE_NAME_BYTE:
+        aFs.setByteValue(aFeature, (byte) aParser.getValueAsInt());
+        break;
+      case TYPE_NAME_INTEGER:
+        int value = aParser.getValueAsInt();
+        value = convertOffsetsIfNecessary(aCtxt, aFs, aFeature, value, fieldType);
+        aFs.setIntValue(aFeature, value);
+        break;
+      case TYPE_NAME_LONG:
+        aFs.setLongValue(aFeature, aParser.getValueAsLong());
+        break;
+      case TYPE_NAME_SHORT:
+        aFs.setShortValue(aFeature, (short) aParser.getValueAsInt());
+        break;
+      default:
+        throw new JsonParseException(aParser, "Feature of type " + aFeature.getRange().getName()
+                + " cannot be set from a JSON value of type " + aParser.currentToken());
+    }
+  }
+
+  private int convertOffsetsIfNecessary(DeserializationContext aCtxt, FeatureStructure aFs,
+          Feature aFeature, int aValue, FieldType fieldType) {
+    if (aFs instanceof Annotation && (CAS.FEATURE_FULL_NAME_BEGIN.equals(aFeature.getName())
+            || CAS.FEATURE_FULL_NAME_END.equals(aFeature.getName())
+            || fieldType == FieldType.ANCHOR)) {
+      Annotation ann = (Annotation) aFs;
+      return OffsetConversionMode.getConverter(aCtxt, ann.getSofa().getSofaID()) //
+              .map(conv -> conv.mapExternal(aValue)) //
+              .orElse(aValue);
+    }
+
+    return aValue;
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructureSerializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructureSerializer.java
new file mode 100644
index 0000000..bf29678
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructureSerializer.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.NUMBER_FLOAT_NAN;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.NUMBER_FLOAT_NEGATIVE_INFINITY;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.NUMBER_FLOAT_POSITIVE_INFINITY;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.NUMERIC_FEATURE_PREFIX;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.REF_FEATURE_PREFIX;
+
+import java.io.IOException;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Feature;
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.jcas.tcas.Annotation;
+import org.apache.uima.json.jsoncas2.mode.OffsetConversionMode;
+import org.apache.uima.json.jsoncas2.ref.ReferenceCache;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.DatabindContext;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+public class FeatureStructureSerializer
+        extends FeatureStructureSerializer_ImplBase<FeatureStructure> {
+  private static final long serialVersionUID = -5346232657650250679L;
+
+  public FeatureStructureSerializer() {
+    super(FeatureStructure.class);
+  }
+
+  protected FeatureStructureSerializer(Class<? extends FeatureStructure> aClazz) {
+    super((Class<FeatureStructure>) aClazz);
+  }
+
+  @Override
+  protected void writeBody(SerializerProvider aProvider, JsonGenerator aJg, FeatureStructure aFs)
+          throws IOException {
+    ReferenceCache refCache = ReferenceCache.get(aProvider);
+    for (Feature feature : aFs.getType().getFeatures()) {
+      writeFeature(aProvider, refCache, aJg, aFs, feature);
+    }
+  }
+
+  protected void writeFeature(SerializerProvider aProvider, ReferenceCache aRefCache,
+          JsonGenerator aJg, FeatureStructure aFs, Feature aFeature) throws IOException {
+    if (!aFeature.getRange().isPrimitive()) {
+      FeatureStructure target = aFs.getFeatureValue(aFeature);
+      if (target != null) {
+        aJg.writeNumberField(REF_FEATURE_PREFIX + aFeature.getShortName(),
+                aRefCache.fsRef(aFs.getFeatureValue(aFeature)));
+      }
+      return;
+    }
+
+    if (aFeature.getRange().isStringOrStringSubtype()) {
+      String value = aFs.getStringValue(aFeature);
+      if (value != null) {
+        aJg.writeStringField(aFeature.getShortName(), value);
+      }
+
+      return;
+    }
+
+    String rangeTypeName = aFeature.getRange().getName();
+    switch (rangeTypeName) {
+      case CAS.TYPE_NAME_BOOLEAN:
+        aJg.writeBooleanField(aFeature.getShortName(), aFs.getBooleanValue(aFeature));
+        break;
+      case CAS.TYPE_NAME_BYTE:
+        aJg.writeNumberField(aFeature.getShortName(), aFs.getByteValue(aFeature));
+        break;
+      case CAS.TYPE_NAME_DOUBLE:
+        writeFloatingPointField(aJg, aFeature.getShortName(), aFs.getDoubleValue(aFeature));
+        break;
+      case CAS.TYPE_NAME_FLOAT:
+        writeFloatingPointField(aJg, aFeature.getShortName(), aFs.getFloatValue(aFeature));
+        break;
+      case CAS.TYPE_NAME_INTEGER: {
+        aJg.writeFieldName(aFeature.getShortName());
+        int value = aFs.getIntValue(aFeature);
+        value = convertOffsetsIfNecessary(aProvider, aFs, aFeature, value);
+        aJg.writeNumber(value);
+        break;
+      }
+      case CAS.TYPE_NAME_LONG:
+        aJg.writeNumberField(aFeature.getShortName(), aFs.getLongValue(aFeature));
+        break;
+      case CAS.TYPE_NAME_SHORT:
+        aJg.writeNumberField(aFeature.getShortName(), aFs.getShortValue(aFeature));
+        break;
+      default:
+        throw new IOException("Unsupported primitive type [" + rangeTypeName + "]");
+    }
+  }
+
+  private void writeFloatingPointField(JsonGenerator aJg, String aFeatureName, double aValue)
+          throws IOException {
+    if (Double.isNaN(aValue)) {
+      aJg.writeStringField(NUMERIC_FEATURE_PREFIX + aFeatureName, NUMBER_FLOAT_NAN);
+    } else if (aValue == Double.NEGATIVE_INFINITY) {
+      aJg.writeStringField(NUMERIC_FEATURE_PREFIX + aFeatureName, NUMBER_FLOAT_NEGATIVE_INFINITY);
+    } else if (aValue == Double.POSITIVE_INFINITY) {
+      aJg.writeStringField(NUMERIC_FEATURE_PREFIX + aFeatureName, NUMBER_FLOAT_POSITIVE_INFINITY);
+    } else {
+      aJg.writeNumberField(aFeatureName, aValue);
+    }
+  }
+
+  private int convertOffsetsIfNecessary(DatabindContext aCtxt, FeatureStructure aFs,
+          Feature aFeature, int aValue) {
+    if (aFs instanceof Annotation && (CAS.FEATURE_FULL_NAME_BEGIN.equals(aFeature.getName())
+            || CAS.FEATURE_FULL_NAME_END.equals(aFeature.getName()))) {
+      Annotation ann = (Annotation) aFs;
+      return OffsetConversionMode.getConverter(aCtxt, ann.getSofa().getSofaID()) //
+              .map(conv -> conv.mapInternal(aValue)) //
+              .orElse(aValue);
+    }
+
+    return aValue;
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructureSerializer_ImplBase.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructureSerializer_ImplBase.java
new file mode 100644
index 0000000..e206861
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructureSerializer_ImplBase.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static java.util.Arrays.sort;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.ID_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.TYPE_FIELD;
+
+import java.io.IOException;
+import java.util.Set;
+
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.json.jsoncas2.JsonCas2Names;
+import org.apache.uima.json.jsoncas2.mode.FeatureStructuresMode;
+import org.apache.uima.json.jsoncas2.mode.ViewsMode;
+import org.apache.uima.json.jsoncas2.ref.FeatureStructureToViewIndex;
+import org.apache.uima.json.jsoncas2.ref.ReferenceCache;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+public abstract class FeatureStructureSerializer_ImplBase<T extends FeatureStructure>
+        extends StdSerializer<T> {
+  private static final long serialVersionUID = -7548009399965871335L;
+
+  public FeatureStructureSerializer_ImplBase(Class<T> aClazz) {
+    super(aClazz);
+  }
+
+  @Override
+  public void serialize(T aFs, JsonGenerator jg, SerializerProvider aProvider) throws IOException {
+    ReferenceCache refCache = ReferenceCache.get(aProvider);
+    FeatureStructureToViewIndex fsToViewIndex = FeatureStructureToViewIndex.get(aProvider);
+    ViewsMode viewsMode = ViewsMode.get(aProvider);
+    FeatureStructuresMode featureStructuresMode = FeatureStructuresMode.get(aProvider);
+
+    jg.writeStartObject();
+    if (featureStructuresMode == FeatureStructuresMode.AS_ARRAY) {
+      jg.writeNumberField(ID_FIELD, refCache.fsRef(aFs));
+    }
+    jg.writeStringField(TYPE_FIELD, refCache.typeRef(aFs.getType()));
+
+    if (viewsMode == ViewsMode.INLINE) {
+      Set<String> views = fsToViewIndex.getViewsContainingFs(aFs);
+
+      if (views != null && !views.isEmpty()) {
+        String[] viewsArray = views.toArray(new String[views.size()]);
+        sort(viewsArray);
+        jg.writeArrayFieldStart(JsonCas2Names.VIEWS_FIELD);
+        for (String view : viewsArray) {
+          jg.writeString(view);
+        }
+        jg.writeEndArray();
+      }
+    }
+
+    // List<String> flags = new ArrayList<>();
+    // if (((CASImpl) aFs.getCAS()).getDocumentAnnotationNoCreate() == aFs) {
+    // flags.add(FLAG_DOCUMENT_ANNOTATION);
+    // }
+    //
+    // if (!flags.isEmpty()) {
+    // jg.writeArrayFieldStart(JsonCas2Names.FLAGS_FIELD);
+    // for (String flag : flags) {
+    // jg.writeString(flag);
+    // }
+    // jg.writeEndArray();
+    // }
+
+    writeBody(aProvider, jg, aFs);
+
+    jg.writeEndObject();
+  }
+
+  protected abstract void writeBody(SerializerProvider aProvider, JsonGenerator jg,
+          FeatureStructure aFs) throws IOException;
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructuresAsArrayDeserializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructuresAsArrayDeserializer.java
new file mode 100644
index 0000000..001244b
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructuresAsArrayDeserializer.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.json.jsoncas2.model.FeatureStructures;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+
+public class FeatureStructuresAsArrayDeserializer
+        extends CasDeserializer_ImplBase<FeatureStructures> {
+  private static final long serialVersionUID = -5937326876753347248L;
+
+  public FeatureStructuresAsArrayDeserializer() {
+    super(FeatureStructures.class);
+  }
+
+  @Override
+  public FeatureStructures deserialize(JsonParser aParser, DeserializationContext aCtxt)
+          throws IOException, JsonProcessingException {
+
+    // Consume array begin
+    aParser.nextToken();
+
+    List<FeatureStructure> featureStructures = new ArrayList<>();
+    while (aParser.currentToken() != JsonToken.END_ARRAY) {
+      featureStructures.add(aCtxt.readValue(aParser, FeatureStructure.class));
+      aParser.nextToken();
+    }
+
+    runPostprocessors(aCtxt);
+
+    // Consume array end
+    aParser.nextValue();
+
+    return new FeatureStructures(featureStructures);
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructuresAsArraySerializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructuresAsArraySerializer.java
new file mode 100644
index 0000000..43d94b0
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructuresAsArraySerializer.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import java.io.IOException;
+
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.json.jsoncas2.model.FeatureStructures;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+public class FeatureStructuresAsArraySerializer extends StdSerializer<FeatureStructures> {
+  private static final long serialVersionUID = 4848917731920209133L;
+
+  public FeatureStructuresAsArraySerializer() {
+    super(FeatureStructures.class);
+  }
+
+  @Override
+  public void serialize(FeatureStructures aFeatureStructures, JsonGenerator jg,
+          SerializerProvider aProvider) throws IOException {
+    if (aFeatureStructures.isEmpty()) {
+      jg.writeStartArray();
+      jg.writeEndArray();
+      return;
+    }
+
+    jg.writeStartArray();
+
+    for (FeatureStructure fs : aFeatureStructures) {
+      aProvider.defaultSerializeValue(fs, jg);
+    }
+    jg.writeEndArray();
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructuresAsObjectDeserializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructuresAsObjectDeserializer.java
new file mode 100644
index 0000000..977bdb8
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructuresAsObjectDeserializer.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.json.jsoncas2.model.FeatureStructures;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+
+public class FeatureStructuresAsObjectDeserializer
+        extends CasDeserializer_ImplBase<FeatureStructures> {
+  private static final long serialVersionUID = -5937326876753347248L;
+
+  public FeatureStructuresAsObjectDeserializer() {
+    super(FeatureStructures.class);
+  }
+
+  @Override
+  public FeatureStructures deserialize(JsonParser aParser, DeserializationContext aCtxt)
+          throws IOException, JsonProcessingException {
+
+    // Consume object begin
+    aParser.nextValue();
+
+    List<FeatureStructure> featureStructures = new ArrayList<>();
+    while (aParser.currentToken() != JsonToken.END_OBJECT) {
+      featureStructures.add(aCtxt.readValue(aParser, FeatureStructure.class));
+      aParser.nextValue();
+    }
+
+    runPostprocessors(aCtxt);
+
+    // Consume object end
+    aParser.nextValue();
+
+    return new FeatureStructures(featureStructures);
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructuresAsObjectSerializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructuresAsObjectSerializer.java
new file mode 100644
index 0000000..6dce30e
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/FeatureStructuresAsObjectSerializer.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import java.io.IOException;
+
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.json.jsoncas2.model.FeatureStructures;
+import org.apache.uima.json.jsoncas2.ref.ReferenceCache;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+public class FeatureStructuresAsObjectSerializer extends StdSerializer<FeatureStructures> {
+  private static final long serialVersionUID = 4848917731920209133L;
+
+  public FeatureStructuresAsObjectSerializer() {
+    super(FeatureStructures.class);
+  }
+
+  @Override
+  public void serialize(FeatureStructures aFeatureStructures, JsonGenerator jg,
+          SerializerProvider aProvider) throws IOException {
+    ReferenceCache refCache = ReferenceCache.get(aProvider);
+
+    jg.writeStartObject();
+
+    for (FeatureStructure fs : aFeatureStructures) {
+      jg.writeFieldName(Integer.toString(refCache.fsRef(fs)));
+      aProvider.defaultSerializeValue(fs, jg);
+    }
+
+    jg.writeEndObject();
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/SofaSerializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/SofaSerializer.java
new file mode 100644
index 0000000..92e72b8
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/SofaSerializer.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.VIEW_MEMBERS_FIELD;
+
+import java.io.IOException;
+
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.jcas.cas.Sofa;
+import org.apache.uima.jcas.cas.TOP;
+import org.apache.uima.json.jsoncas2.ref.ReferenceCache;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+@Deprecated
+public class SofaSerializer extends FeatureStructureSerializer {
+  private static final long serialVersionUID = -5346232657650250679L;
+
+  public SofaSerializer() {
+    super(Sofa.class);
+  }
+
+  @Override
+  protected void writeBody(SerializerProvider aProvider, JsonGenerator jg, FeatureStructure aFs)
+          throws IOException {
+    super.writeBody(aProvider, jg, aFs);
+
+    ReferenceCache refCache = ReferenceCache.get(aProvider);
+    Sofa sofa = (Sofa) aFs;
+
+    jg.writeFieldName(VIEW_MEMBERS_FIELD);
+    jg.writeStartArray();
+    for (TOP fs : sofa.getCAS().getView(sofa.getSofaID()).getIndexedFSs()) {
+      jg.writeNumber(refCache.fsRef(fs));
+    }
+    jg.writeEndArray();
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/TypeDeserializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/TypeDeserializer.java
new file mode 100644
index 0000000..77959e6
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/TypeDeserializer.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.uima.UIMAFramework;
+import org.apache.uima.json.jsoncas2.JsonCas2Names;
+import org.apache.uima.resource.metadata.TypeDescription;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class TypeDeserializer extends CasDeserializer_ImplBase<TypeDescription> {
+  private static final long serialVersionUID = -3406515095847310810L;
+
+  public TypeDeserializer() {
+    super(TypeDescription.class);
+  }
+
+  @Override
+  public TypeDescription deserialize(JsonParser aParser, DeserializationContext aCtxt)
+          throws IOException, JsonProcessingException {
+    JsonNode node = aParser.readValueAsTree();
+
+    if (!node.isObject()) {
+      throw new JsonParseException(aParser, "Type system declaration must be a JSON object");
+    }
+
+    String typeName = node.get(JsonCas2Names.NAME_FIELD).asText();
+    String parentTypeName = node.get(JsonCas2Names.SUPER_TYPE_FIELD).asText();
+    Optional<String> componentType = Optional.ofNullable(node.get(JsonCas2Names.ELEMENT_TYPE_FIELD))
+            .map(JsonNode::asText);
+
+    List<String> featureNames = new ArrayList<>();
+    node.fieldNames().forEachRemaining(name -> {
+      if (!name.startsWith(JsonCas2Names.RESERVED_FIELD_PREFIX)) {
+        featureNames.add(name);
+      }
+    });
+
+    TypeDescription td = UIMAFramework.getResourceSpecifierFactory().createTypeDescription();
+    td.setName(typeName);
+    td.setSupertypeName(parentTypeName);
+    // td.setDescription("");
+    // td.setAllowedValues(null);
+    // td.setFeatures(null);
+    // td.setSourceUrl(aParser.getTokenLocation().sourceDescription());
+    return td;
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/TypeSerializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/TypeSerializer.java
new file mode 100644
index 0000000..75553d7
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/TypeSerializer.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static java.util.stream.Collectors.toList;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.ELEMENT_TYPE_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.SUPER_TYPE_FIELD;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.uima.cas.Feature;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.impl.TypeImpl;
+import org.apache.uima.json.jsoncas2.JsonCas2Names;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+public class TypeSerializer extends StdSerializer<Type> {
+  private static final long serialVersionUID = 8549058399080277660L;
+
+  public TypeSerializer() {
+    super(Type.class);
+  }
+
+  @Override
+  public void serialize(Type aType, JsonGenerator aJg, SerializerProvider aProvider)
+          throws IOException {
+    aJg.writeStartObject(aType);
+
+    aJg.writeStringField(JsonCas2Names.NAME_FIELD, aType.getName());
+
+    Type parent = ((TypeImpl) aType).getSuperType();
+    if (parent != null) {
+      aJg.writeStringField(SUPER_TYPE_FIELD, parent.getName());
+    }
+
+    if (aType.getComponentType() != null) {
+      aJg.writeStringField(ELEMENT_TYPE_FIELD, aType.getComponentType().getName());
+    }
+
+    List<Feature> newFeatures = aType.getFeatures().stream().filter(f -> f.getDomain() == aType)
+            .collect(toList());
+    if (!newFeatures.isEmpty()) {
+      for (Feature feature : newFeatures) {
+        aJg.writeFieldName(feature.getShortName());
+        aProvider.defaultSerializeValue(feature, aJg);
+      }
+    }
+
+    aJg.writeEndObject();
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/TypeSystemDeserializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/TypeSystemDeserializer.java
new file mode 100644
index 0000000..c20aa3f
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/TypeSystemDeserializer.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import org.apache.uima.UIMAFramework;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.resource.metadata.TypeSystemDescription;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class TypeSystemDeserializer extends CasDeserializer_ImplBase<TypeSystemDescription> {
+  private static final long serialVersionUID = 7137336340824618031L;
+
+  public TypeSystemDeserializer() {
+    super(TypeSystemDescription.class);
+  }
+
+  @Override
+  public TypeSystemDescription deserialize(JsonParser aParser, DeserializationContext aCtxt)
+          throws IOException, JsonProcessingException {
+    JsonNode node = aParser.readValueAsTree();
+
+    if (!node.isObject()) {
+      throw new JsonParseException(aParser, "Type system declaration must be a JSON object");
+    }
+
+    CAS cas = getCas(aCtxt);
+
+    TypeSystemDescription tsd = UIMAFramework.getResourceSpecifierFactory()
+            .createTypeSystemDescription();
+
+    Iterator<String> typeNameIterator = node.fieldNames();
+    while (typeNameIterator.hasNext()) {
+      String typeName = typeNameIterator.next();
+      node.get(typeName);
+      // FIXME !!!
+    }
+
+    return tsd;
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/TypeSystemSerializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/TypeSystemSerializer.java
new file mode 100644
index 0000000..7be06ca
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/TypeSystemSerializer.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.unmodifiableSet;
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.StreamSupport;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.TypeSystem;
+import org.apache.uima.json.jsoncas2.ref.ReferenceCache;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+public class TypeSystemSerializer extends StdSerializer<TypeSystem> {
+  private static final long serialVersionUID = -4369127219437592227L;
+
+  private final Set<String> BUILT_IN_TYPES = unmodifiableSet(new HashSet<>(asList(
+          CAS.TYPE_NAME_ANNOTATION, CAS.TYPE_NAME_ANNOTATION_BASE, CAS.TYPE_NAME_ARRAY_BASE,
+          CAS.TYPE_NAME_BOOLEAN, CAS.TYPE_NAME_BOOLEAN_ARRAY, CAS.TYPE_NAME_BYTE,
+          CAS.TYPE_NAME_BYTE_ARRAY, CAS.TYPE_NAME_DOCUMENT_ANNOTATION, CAS.TYPE_NAME_DOUBLE,
+          CAS.TYPE_NAME_DOUBLE_ARRAY, CAS.TYPE_NAME_EMPTY_FLOAT_LIST, CAS.TYPE_NAME_EMPTY_FS_LIST,
+          CAS.TYPE_NAME_EMPTY_INTEGER_LIST, CAS.TYPE_NAME_EMPTY_STRING_LIST, CAS.TYPE_NAME_FLOAT,
+          CAS.TYPE_NAME_FLOAT_ARRAY, CAS.TYPE_NAME_FLOAT_LIST, CAS.TYPE_NAME_FS_ARRAY,
+          CAS.TYPE_NAME_FS_LIST, CAS.TYPE_NAME_INTEGER, CAS.TYPE_NAME_INTEGER_ARRAY,
+          CAS.TYPE_NAME_INTEGER_LIST, CAS.TYPE_NAME_LIST_BASE, CAS.TYPE_NAME_LONG,
+          CAS.TYPE_NAME_LONG_ARRAY, CAS.TYPE_NAME_NON_EMPTY_FLOAT_LIST,
+          CAS.TYPE_NAME_NON_EMPTY_FS_LIST, CAS.TYPE_NAME_NON_EMPTY_INTEGER_LIST,
+          CAS.TYPE_NAME_NON_EMPTY_STRING_LIST, CAS.TYPE_NAME_SHORT, CAS.TYPE_NAME_SHORT_ARRAY,
+          CAS.TYPE_NAME_SOFA, CAS.TYPE_NAME_STRING, CAS.TYPE_NAME_STRING_ARRAY,
+          CAS.TYPE_NAME_STRING_LIST, CAS.TYPE_NAME_TOP)));
+
+  public TypeSystemSerializer() {
+    super(TypeSystem.class);
+  }
+
+  @Override
+  public void serialize(TypeSystem aTypeSystem, JsonGenerator jg, SerializerProvider aProvider)
+          throws IOException {
+    ReferenceCache refCache = ReferenceCache.get(aProvider);
+
+    jg.writeStartObject(aTypeSystem);
+
+    List<Type> types = StreamSupport.stream(aTypeSystem.spliterator(), false)
+            .sorted(comparing(Type::getName))
+            .filter(type -> !BUILT_IN_TYPES.contains(type.getName())).collect(toList());
+
+    for (Type type : types) {
+      jg.writeFieldName(refCache.typeRef(type));
+
+      aProvider.defaultSerializeValue(type, jg);
+    }
+
+    jg.writeEndObject();
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/ViewsDeserializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/ViewsDeserializer.java
new file mode 100644
index 0000000..8b6c0e8
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/ViewsDeserializer.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import java.io.IOException;
+
+import org.apache.uima.json.jsoncas2.JsonCas2Names;
+import org.apache.uima.json.jsoncas2.model.Views;
+import org.apache.uima.json.jsoncas2.ref.FeatureStructureIdToViewIndex;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+
+public class ViewsDeserializer extends CasDeserializer_ImplBase<Views> {
+  private static final long serialVersionUID = -2976455559005753544L;
+
+  public ViewsDeserializer() {
+    super(Views.class);
+  }
+
+  @Override
+  public Views deserialize(JsonParser aParser, DeserializationContext aCtxt)
+          throws IOException, JsonProcessingException {
+    if (aParser.currentToken() != JsonToken.START_OBJECT) {
+      throw new JsonParseException(aParser, "Views declaration must be a JSON object");
+    }
+
+    // Move to the first view if there is one
+    aParser.nextValue();
+    while (aParser.currentToken() != JsonToken.END_OBJECT) {
+      String viewName = aParser.getCurrentName();
+      deserializeView(aParser, aCtxt, viewName);
+      aParser.nextValue();
+    }
+
+    return new Views(getCas(aCtxt));
+  }
+
+  private void deserializeView(JsonParser aParser, DeserializationContext aCtxt, String aViewName)
+          throws IOException {
+    while (aParser.currentToken() != JsonToken.END_OBJECT) {
+      aParser.nextValue();
+      String fieldName = aParser.getCurrentName();
+
+      switch (fieldName) {
+        case JsonCas2Names.VIEW_SOFA_FIELD:
+          // Ignore
+          break;
+        case JsonCas2Names.VIEW_MEMBERS_FIELD:
+          deserializeIndex(aParser, aCtxt, aViewName);
+          break;
+      }
+    }
+  }
+
+  private void deserializeIndex(JsonParser aParser, DeserializationContext aCtxt, String aViewName)
+          throws IOException {
+    FeatureStructureIdToViewIndex fsIdToViewIdx = FeatureStructureIdToViewIndex.get(aCtxt);
+    aParser.nextToken();
+    while (aParser.currentToken() != JsonToken.END_ARRAY) {
+      fsIdToViewIdx.assignFsToView(aParser.getIntValue(), aViewName);
+      aParser.nextValue();
+    }
+    aParser.nextToken();
+  }
+}
diff --git a/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/ViewsSerializer.java b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/ViewsSerializer.java
new file mode 100644
index 0000000..090c8f3
--- /dev/null
+++ b/uimaj-json/src/main/java/org/apache/uima/json/jsoncas2/ser/ViewsSerializer.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.VIEW_MEMBERS_FIELD;
+import static org.apache.uima.json.jsoncas2.JsonCas2Names.VIEW_SOFA_FIELD;
+
+import java.io.IOException;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.json.jsoncas2.mode.SofaMode;
+import org.apache.uima.json.jsoncas2.model.Views;
+import org.apache.uima.json.jsoncas2.ref.ReferenceCache;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+public class ViewsSerializer extends StdSerializer<Views> {
+  private static final long serialVersionUID = 7530813663936058935L;
+
+  public ViewsSerializer() {
+    super(Views.class);
+  }
+
+  @Override
+  public void serialize(Views aViews, JsonGenerator jg, SerializerProvider aProvider)
+          throws IOException {
+    ReferenceCache refCache = ReferenceCache.get(aProvider);
+    SofaMode sofaMode = SofaMode.get(aProvider);
+
+    jg.writeStartObject();
+
+    for (CAS view : aViews) {
+      jg.writeFieldName(view.getViewName());
+
+      jg.writeStartObject();
+
+      switch (sofaMode) {
+        case AS_PART_OF_VIEW:
+          jg.writeFieldName(VIEW_SOFA_FIELD);
+          aProvider.defaultSerializeValue(view.getSofa(), jg);
+          break;
+        case AS_REGULAR_FEATURE_STRUCTURE:
+          jg.writeNumberField(VIEW_SOFA_FIELD, refCache.fsRef(view.getSofa()));
+          break;
+      }
+
+      jg.writeFieldName(VIEW_MEMBERS_FIELD);
+      jg.writeStartArray();
+      for (int fsId : view.getIndexedFSs().stream().mapToInt(refCache::fsRef).sorted().toArray()) {
+        jg.writeNumber(fsId);
+      }
+      jg.writeEndArray();
+
+      jg.writeEndObject();
+    }
+
+    jg.writeEndObject();
+  }
+}
diff --git a/uimaj-json/src/test/java/org/apache/uima/json/CasSerializationDeserialization_Json_Test.java b/uimaj-json/src/test/java/org/apache/uima/json/CasSerializationDeserialization_Json_Test.java
new file mode 100644
index 0000000..71b1979
--- /dev/null
+++ b/uimaj-json/src/test/java/org/apache/uima/json/CasSerializationDeserialization_Json_Test.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json;
+
+import static java.util.stream.Collectors.toList;
+import static org.apache.uima.cas.serdes.TestType.ONE_WAY;
+import static org.apache.uima.cas.serdes.TestType.SER_REF;
+import static org.apache.uima.cas.serdes.datasuites.XmiFileDataSuite.XMI_SUITE_BASE_PATH;
+import static org.apache.uima.json.JsonCasSerializer.jsonSerialize;
+
+import java.nio.file.Paths;
+import java.util.List;
+
+import org.apache.uima.cas.serdes.datasuites.ProgrammaticallyCreatedCasDataSuite;
+import org.apache.uima.cas.serdes.datasuites.XmiFileDataSuite;
+import org.apache.uima.cas.serdes.scenario.SerRefTestScenario;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class CasSerializationDeserialization_Json_Test {
+
+  private static final String CAS_FILE_NAME = "data.json";
+  // private static final int RANDOM_CAS_ITERATIONS = 20;
+  //
+  // private static final List<CasSerDesCycleConfiguration> serDesCycles = asList( //
+  // new CasSerDesCycleConfiguration(FORMAT + " / DEFAULT", //
+  // (a, b) -> serdes(a, b, FORMAT, DEFAULT)),
+  // new CasSerDesCycleConfiguration(FORMAT + " / LENIENT", //
+  // (a, b) -> serdes(a, b, FORMAT, LENIENT)));
+  //
+  // private static final List<CasDesSerCycleConfiguration> desSerCycles = asList( //
+  // new CasDesSerCycleConfiguration(FORMAT + " / DEFAULT", //
+  // (a, b) -> desser(createCas(), a, b, FORMAT, DEFAULT)),
+  // new CasDesSerCycleConfiguration(FORMAT + " / LENIENT", //
+  // (a, b) -> desser(createCas(), a, b, FORMAT, LENIENT)));
+
+  private static List<SerRefTestScenario> serRefScenarios() {
+    Class<?> caller = CasSerializationDeserialization_Json_Test.class;
+    return ProgrammaticallyCreatedCasDataSuite.builder().build().stream()
+            .map(conf -> SerRefTestScenario.builder(caller, conf, SER_REF, CAS_FILE_NAME)
+                    .withSerializer((cas, path) -> jsonSerialize(cas, null, path.toFile(), true,
+                            null, null)) //
+                    .build())
+            .collect(toList());
+  }
+
+  private static List<SerRefTestScenario> oneWayDesSerScenarios() throws Exception {
+    Class<?> caller = CasSerializationDeserialization_Json_Test.class;
+    return XmiFileDataSuite
+            .configurations(Paths.get("..", "uimaj-core").resolve(XMI_SUITE_BASE_PATH)).stream()
+            .map(conf -> SerRefTestScenario.builder(caller, conf, ONE_WAY, CAS_FILE_NAME)
+                    .withSerializer((cas, path) -> jsonSerialize(cas, null, path.toFile(), true,
+                            null, null)) //
+                    .build())
+            .collect(toList());
+  }
+
+  // private static List<DesSerTestScenario> roundTripDesSerScenarios() throws Exception {
+  // return SerDesCasIOTestUtils.roundTripDesSerScenarios(desSerCycles, CAS_FILE_NAME);
+  // }
+  //
+  // private static List<SerDesTestScenario> serDesScenarios() {
+  // return SerDesCasIOTestUtils.serDesScenarios(serDesCycles);
+  // }
+  //
+  // private static List<SerDesTestScenario> randomSerDesScenarios() {
+  // return SerDesCasIOTestUtils.randomSerDesScenarios(serDesCycles, RANDOM_CAS_ITERATIONS);
+  // }
+
+  @ParameterizedTest
+  @MethodSource("serRefScenarios")
+  public void serializeAndCompareToReferenceTest(Runnable aScenario) throws Exception {
+    aScenario.run();
+  }
+
+  @ParameterizedTest
+  @MethodSource("oneWayDesSerScenarios")
+  public void oneWayDeserializeSerializeTest(Runnable aScenario) throws Exception {
+    aScenario.run();
+  }
+
+  // @ParameterizedTest
+  // @MethodSource("serDesScenarios")
+  // public void serializeDeserializeTest(Runnable aScenario) throws Exception {
+  // aScenario.run();
+  // }
+  //
+  // @ParameterizedTest
+  // @MethodSource("randomSerDesScenarios")
+  // public void randomizedSerializeDeserializeTest(Runnable aScenario) throws Exception {
+  // aScenario.run();
+  // }
+  //
+  // @ParameterizedTest
+  // @MethodSource("roundTripDesSerScenarios")
+  // public void roundTripDeserializeSerializeTest(Runnable aScenario) throws Exception {
+  // aScenario.run();
+  // }
+}
diff --git a/uimaj-json/src/test/java/org/apache/uima/json/flexjson/FlexJsonCasDeserializeSerializeTest.java b/uimaj-json/src/test/java/org/apache/uima/json/flexjson/FlexJsonCasDeserializeSerializeTest.java
new file mode 100644
index 0000000..7b1f4e5
--- /dev/null
+++ b/uimaj-json/src/test/java/org/apache/uima/json/flexjson/FlexJsonCasDeserializeSerializeTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.flexjson;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.asList;
+import static org.apache.uima.util.CasCreationUtils.createCas;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.contentOf;
+
+import java.io.File;
+
+import org.apache.uima.cas.CAS;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+@Deprecated
+@Ignore("The deserializer is not really implemented...")
+@RunWith(value = Parameterized.class)
+public class FlexJsonCasDeserializeSerializeTest {
+  @Parameters(name = "{index}: running on file {0}")
+  public static Iterable<File> tsvFiles() {
+    return asList(new File("src/test/resources/FlexJsonSerializerTest/")
+            .listFiles(file -> file.isDirectory()));
+  }
+
+  private CAS cas;
+  private File referenceFolder;
+  private File referenceFile;
+  private File outputFile;
+  private JsonFactory jsonFactory;
+
+  public FlexJsonCasDeserializeSerializeTest(File aFolder) throws Exception {
+    referenceFolder = aFolder;
+    referenceFile = new File(referenceFolder, "reference.json");
+    outputFile = new File("target/test-output/" + getClass().getSimpleName() + "/"
+            + referenceFolder.getName() + "/output.json");
+    outputFile.getParentFile().mkdirs();
+
+    cas = createCas();
+
+    jsonFactory = new JsonFactory();
+    jsonFactory.setCodec(new ObjectMapper());
+  }
+
+  @Test
+  public void testReadWrite() throws Exception {
+    FlexJsonCasDeserializer deser = new FlexJsonCasDeserializer(
+            jsonFactory.createParser(referenceFile));
+    deser.read(cas);
+
+    FlexJsonCasSerializer.builder().write(cas, outputFile);
+
+    assertThat(contentOf(outputFile, UTF_8)).isEqualTo(contentOf(referenceFile, UTF_8));
+    // assertEquals(contentOf(referenceFile, UTF_8), contentOf(outputFile, UTF_8), STRICT);
+  }
+}
diff --git a/uimaj-json/src/test/java/org/apache/uima/json/flexjson/FlexJsonDeserializerTest.java b/uimaj-json/src/test/java/org/apache/uima/json/flexjson/FlexJsonDeserializerTest.java
new file mode 100644
index 0000000..532bcf4
--- /dev/null
+++ b/uimaj-json/src/test/java/org/apache/uima/json/flexjson/FlexJsonDeserializerTest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.flexjson;
+
+import static org.apache.uima.util.CasCreationUtils.createCas;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+
+import org.apache.uima.cas.CAS;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+@Deprecated
+public class FlexJsonDeserializerTest {
+  private static CAS cas;
+
+  private JsonFactory jsonFactory;
+
+  @BeforeClass
+  public static void setupOnce() throws Exception {
+    cas = createCas();
+  }
+
+  @Before
+  public void setup() throws Exception {
+    jsonFactory = new JsonFactory();
+    jsonFactory.setCodec(new ObjectMapper());
+  }
+
+  @Test
+  public void thatQuotedStringCanBeParsed() throws Exception {
+    FlexJsonCasDeserializer deser = new FlexJsonCasDeserializer(jsonFactory
+            .createParser(new File("src/test/resources/FlexJsonDeserializer/text_only.json")));
+
+    deser.read(cas);
+
+    assertThat(cas.getDocumentText()).isEqualTo("Hello world.");
+  }
+
+  @Test
+  public void thatFeatureStructureArrayCanBeParsed() throws Exception {
+    FlexJsonCasDeserializer deser = new FlexJsonCasDeserializer(jsonFactory.createParser(
+            new File("src/test/resources/FlexJsonDeserializer/feature_structures_only.json")));
+
+    deser.read(cas);
+
+    assertThat(cas.getDocumentText()).isEqualTo("Hello world.");
+  }
+}
diff --git a/uimaj-json/src/test/java/org/apache/uima/json/flexjson/FlexJsonSerializerTest.java b/uimaj-json/src/test/java/org/apache/uima/json/flexjson/FlexJsonSerializerTest.java
new file mode 100644
index 0000000..ff2ee6c
--- /dev/null
+++ b/uimaj-json/src/test/java/org/apache/uima/json/flexjson/FlexJsonSerializerTest.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.flexjson;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.uima.UIMAFramework.getResourceSpecifierFactory;
+import static org.apache.uima.cas.CAS.TYPE_NAME_ANNOTATION;
+import static org.apache.uima.json.flexjson.FlexJsonCasSerializer.ViewsMode.INLINE;
+import static org.apache.uima.json.flexjson.FlexJsonCasSerializer.ViewsMode.SEPARATE;
+import static org.apache.uima.util.CasCreationUtils.createCas;
+import static org.assertj.core.api.Assertions.contentOf;
+import static org.skyscreamer.jsonassert.JSONAssert.assertEquals;
+import static org.skyscreamer.jsonassert.JSONCompareMode.STRICT;
+
+import java.io.File;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.text.AnnotationFS;
+import org.apache.uima.resource.metadata.TypeDescription;
+import org.apache.uima.resource.metadata.TypeSystemDescription;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestName;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class FlexJsonSerializerTest {
+  public @Rule TemporaryFolder temp = new TemporaryFolder();
+  public @Rule TestName name = new TestName();
+
+  private JsonFactory jsonFactory;
+
+  private File outputFile;
+  private File referenceFile;
+
+  @Before
+  public void setup() throws Exception {
+    jsonFactory = new JsonFactory();
+    jsonFactory.setCodec(new ObjectMapper());
+
+    outputFile = new File("target/test-output/" + getClass().getSimpleName() + "/"
+            + name.getMethodName() + "/output.json");
+    outputFile.getParentFile().mkdirs();
+
+    referenceFile = new File("src/test/resources/" + getClass().getSimpleName() + "/"
+            + name.getMethodName() + "/reference.json");
+  }
+
+  @Test
+  public void multipleViewsAndSofas() throws Exception {
+    CAS cas = createCas();
+    CAS firstView = cas;
+    firstView.setDocumentText("First view");
+    CAS secondView = firstView.createView("secondView");
+    secondView.setDocumentText("Second view");
+
+    FlexJsonCasSerializer.write(cas, outputFile);
+
+    assertEquals(contentOf(referenceFile, UTF_8), contentOf(outputFile, UTF_8), STRICT);
+  }
+
+  @Test
+  public void featureStructureIndexedInMultipleViewsInline() throws Exception {
+    CAS cas = createCas();
+    FeatureStructure fs = cas.createFS(cas.getTypeSystem().getTopType());
+
+    CAS firstView = cas;
+    firstView.setDocumentText("First view");
+    firstView.addFsToIndexes(fs);
+
+    CAS secondView = cas.createView("secondView");
+    secondView.setDocumentText("Second view");
+    secondView.addFsToIndexes(fs);
+
+    FlexJsonCasSerializer.builder() //
+            .setViewsMode(INLINE) //
+            .write(cas, outputFile);
+
+    assertEquals(contentOf(referenceFile, UTF_8), contentOf(outputFile, UTF_8), STRICT);
+  }
+
+  @Test
+  public void featureStructureIndexedInMultipleViewsSeparate() throws Exception {
+    CAS cas = createCas();
+    FeatureStructure fs = cas.createFS(cas.getTypeSystem().getTopType());
+
+    CAS firstView = cas;
+    firstView.setDocumentText("First view");
+    firstView.addFsToIndexes(fs);
+
+    CAS secondView = cas.createView("secondView");
+    secondView.setDocumentText("Second view");
+    secondView.addFsToIndexes(fs);
+
+    FlexJsonCasSerializer.builder() //
+            .setViewsMode(SEPARATE) //
+            .write(cas, outputFile);
+
+    assertEquals(contentOf(referenceFile, UTF_8), contentOf(outputFile, UTF_8), STRICT);
+  }
+
+  @Test
+  public void customAnnotationType() throws Exception {
+    String customTypeName = "custom.Annotation";
+
+    TypeSystemDescription tsd = getResourceSpecifierFactory().createTypeSystemDescription();
+    TypeDescription customTypeDesc = tsd.addType(customTypeName, "", TYPE_NAME_ANNOTATION);
+    customTypeDesc.addFeature("value", "", CAS.TYPE_NAME_STRING);
+
+    CAS cas = createCas(tsd, null, null, null);
+
+    Type customType = cas.getTypeSystem().getType(customTypeName);
+    AnnotationFS fs = cas.createAnnotation(customType, 0, 10);
+    cas.addFsToIndexes(fs);
+
+    FlexJsonCasSerializer.write(cas, outputFile);
+
+    assertEquals(contentOf(referenceFile, UTF_8), contentOf(outputFile, UTF_8), STRICT);
+  }
+}
diff --git a/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/CasSerializationDeserialization_JsonCas2_FsAsArray_Test.java b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/CasSerializationDeserialization_JsonCas2_FsAsArray_Test.java
new file mode 100644
index 0000000..74c53f8
--- /dev/null
+++ b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/CasSerializationDeserialization_JsonCas2_FsAsArray_Test.java
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2;
+
+import static java.lang.invoke.MethodHandles.lookup;
+import static java.util.Arrays.asList;
+import static java.util.stream.Collectors.toList;
+import static org.apache.uima.cas.serdes.SerDesCasIOTestUtils.createCasMaybeWithTypesystem;
+import static org.apache.uima.cas.serdes.TestType.ONE_WAY;
+import static org.apache.uima.cas.serdes.TestType.SER_DES;
+import static org.apache.uima.cas.serdes.TestType.SER_REF;
+import static org.apache.uima.cas.serdes.datasuites.XmiFileDataSuite.XMI_SUITE_BASE_PATH;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.serdes.SerDesCasIOTestUtils;
+import org.apache.uima.cas.serdes.datasuites.MultiFeatureRandomCasDataSuite;
+import org.apache.uima.cas.serdes.datasuites.MultiTypeRandomCasDataSuite;
+import org.apache.uima.cas.serdes.datasuites.ProgrammaticallyCreatedCasDataSuite;
+import org.apache.uima.cas.serdes.datasuites.XmiFileDataSuite;
+import org.apache.uima.cas.serdes.scenario.DesSerTestScenario;
+import org.apache.uima.cas.serdes.scenario.SerDesTestScenario;
+import org.apache.uima.cas.serdes.scenario.SerRefTestScenario;
+import org.apache.uima.cas.serdes.transitions.CasDesSerCycleConfiguration;
+import org.apache.uima.cas.serdes.transitions.CasSerDesCycleConfiguration;
+import org.apache.uima.json.jsoncas2.mode.FeatureStructuresMode;
+import org.apache.uima.json.jsoncas2.mode.OffsetConversionMode;
+import org.apache.uima.json.jsoncas2.mode.SofaMode;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class CasSerializationDeserialization_JsonCas2_FsAsArray_Test {
+
+  private static final String CAS_FILE_NAME = "data.json";
+  private static final int RANDOM_CAS_ITERATIONS = 20;
+
+  private static final List<CasSerDesCycleConfiguration> serDesCycles = asList( //
+          new CasSerDesCycleConfiguration("DEFAULT (default offsets)", //
+                  (a, b) -> serdes(a, b, null)),
+          new CasSerDesCycleConfiguration("DEFAULT (UTF-16 offsets)", //
+                  (a, b) -> serdes(a, b, OffsetConversionMode.UTF_16)),
+          new CasSerDesCycleConfiguration("DEFAULT (UTF-8 offsets)", //
+                  (a, b) -> serdes(a, b, OffsetConversionMode.UTF_8)),
+          new CasSerDesCycleConfiguration("DEFAULT (UTF-32 offsets)", //
+                  (a, b) -> serdes(a, b, OffsetConversionMode.UTF_32)));
+  // new CasSerDesCycleConfiguration(FORMAT + " / LENIENT", //
+  // (a, b) -> serdes(a, b, FORMAT, LENIENT)));
+
+  private static final List<CasDesSerCycleConfiguration> desSerCycles = asList( //
+          new CasDesSerCycleConfiguration("DEFAULT", //
+                  (a, b) -> desser(createCasMaybeWithTypesystem(a), a, b)));
+
+  private static void ser(CAS aSourceCas, Path aTargetCasFile) throws IOException {
+    JsonCas2Serializer serializer = new JsonCas2Serializer();
+    serializer.setFsMode(FeatureStructuresMode.AS_ARRAY);
+    serializer.setSofaMode(SofaMode.AS_REGULAR_FEATURE_STRUCTURE);
+    serializer.serialize(aSourceCas, aTargetCasFile.toFile());
+  }
+
+  private static void des(CAS aTargetCas, Path aSourceCasFile) throws IOException {
+    JsonCas2Deserializer deserializer = new JsonCas2Deserializer();
+    deserializer.setFsMode(FeatureStructuresMode.AS_ARRAY);
+    deserializer.deserialize(aSourceCasFile.toFile(), aTargetCas);
+  }
+
+  public static void serdes(CAS aSourceCas, CAS aTargetCas, OffsetConversionMode aOcm)
+          throws Exception {
+    byte[] buffer;
+    try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+      JsonCas2Serializer serializer = new JsonCas2Serializer();
+      serializer.setFsMode(FeatureStructuresMode.AS_ARRAY);
+      serializer.setSofaMode(SofaMode.AS_REGULAR_FEATURE_STRUCTURE);
+      serializer.setOffsetConversionMode(aOcm);
+      serializer.serialize(aSourceCas, os);
+      buffer = os.toByteArray();
+    }
+
+    Path targetFile = SER_DES.getTargetFolder(lookup().lookupClass()).resolve(CAS_FILE_NAME);
+    Files.createDirectories(targetFile.getParent());
+    try (OutputStream os = Files.newOutputStream(targetFile)) {
+      os.write(buffer);
+    }
+
+    try (InputStream is = new ByteArrayInputStream(buffer)) {
+      JsonCas2Deserializer deserializer = new JsonCas2Deserializer();
+      deserializer.setFsMode(FeatureStructuresMode.AS_ARRAY);
+      deserializer.deserialize(is, aTargetCas);
+    }
+  }
+
+  public static void desser(CAS aBufferCas, Path aSourceCasPath, Path aTargetCasPath)
+          throws Exception {
+    des(aBufferCas, aSourceCasPath);
+    ser(aBufferCas, aTargetCasPath);
+  }
+
+  private static List<SerRefTestScenario> serRefScenarios() {
+    Class<?> caller = CasSerializationDeserialization_JsonCas2_FsAsArray_Test.class;
+    return ProgrammaticallyCreatedCasDataSuite.builder().build().stream()
+            .map(conf -> SerRefTestScenario.builder(caller, conf, SER_REF, CAS_FILE_NAME)
+                    .withSerializer((cas, path) -> ser(cas, path)).build())
+            .collect(toList());
+  }
+
+  private static List<SerRefTestScenario> oneWayDesSerScenarios() throws Exception {
+    Class<?> caller = CasSerializationDeserialization_JsonCas2_FsAsArray_Test.class;
+    return XmiFileDataSuite
+            .configurations(Paths.get("..", "uimaj-core").resolve(XMI_SUITE_BASE_PATH)).stream()
+            .map(conf -> SerRefTestScenario.builder(caller, conf, ONE_WAY, CAS_FILE_NAME)
+                    .withSerializer((cas, path) -> ser(cas, path)).build())
+            .collect(toList());
+  }
+
+  private static List<DesSerTestScenario> roundTripDesSerScenarios() throws Exception {
+    return SerDesCasIOTestUtils.roundTripDesSerScenariosComparingFileContents(desSerCycles,
+            CAS_FILE_NAME);
+  }
+
+  private static List<SerDesTestScenario> serDesScenarios() {
+    return SerDesCasIOTestUtils.programmaticSerDesScenarios(serDesCycles);
+  }
+
+  private static List<SerDesTestScenario> randomSerDesScenarios() {
+    return SerDesCasIOTestUtils.serDesScenarios(serDesCycles,
+            MultiFeatureRandomCasDataSuite.builder().withIterations(RANDOM_CAS_ITERATIONS).build(),
+            MultiTypeRandomCasDataSuite.builder().withIterations(RANDOM_CAS_ITERATIONS).build());
+  }
+
+  @ParameterizedTest
+  @MethodSource("serRefScenarios")
+  public void serializeAndCompareToReferenceTest(Runnable aScenario) throws Exception {
+    aScenario.run();
+  }
+
+  @ParameterizedTest
+  @MethodSource("oneWayDesSerScenarios")
+  public void oneWayDeserializeSerializeTest(Runnable aScenario) throws Exception {
+    aScenario.run();
+  }
+
+  @ParameterizedTest
+  @MethodSource("serDesScenarios")
+  public void serializeDeserializeTest(Runnable aScenario) throws Exception {
+    aScenario.run();
+  }
+
+  @ParameterizedTest
+  @MethodSource("randomSerDesScenarios")
+  public void randomizedSerializeDeserializeTest(Runnable aScenario) throws Exception {
+    aScenario.run();
+  }
+
+  @ParameterizedTest
+  @MethodSource("roundTripDesSerScenarios")
+  public void roundTripDeserializeSerializeTest(Runnable aScenario) throws Exception {
+    aScenario.run();
+  }
+}
diff --git a/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/CasSerializationDeserialization_JsonCas2_FsAsObject_Test.java b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/CasSerializationDeserialization_JsonCas2_FsAsObject_Test.java
new file mode 100644
index 0000000..5ad8145
--- /dev/null
+++ b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/CasSerializationDeserialization_JsonCas2_FsAsObject_Test.java
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2;
+
+import static java.lang.invoke.MethodHandles.lookup;
+import static java.util.Arrays.asList;
+import static java.util.stream.Collectors.toList;
+import static org.apache.uima.cas.serdes.SerDesCasIOTestUtils.createCasMaybeWithTypesystem;
+import static org.apache.uima.cas.serdes.TestType.ONE_WAY;
+import static org.apache.uima.cas.serdes.TestType.SER_DES;
+import static org.apache.uima.cas.serdes.TestType.SER_REF;
+import static org.apache.uima.cas.serdes.datasuites.XmiFileDataSuite.XMI_SUITE_BASE_PATH;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.serdes.SerDesCasIOTestUtils;
+import org.apache.uima.cas.serdes.datasuites.MultiFeatureRandomCasDataSuite;
+import org.apache.uima.cas.serdes.datasuites.MultiTypeRandomCasDataSuite;
+import org.apache.uima.cas.serdes.datasuites.ProgrammaticallyCreatedCasDataSuite;
+import org.apache.uima.cas.serdes.datasuites.XmiFileDataSuite;
+import org.apache.uima.cas.serdes.scenario.DesSerTestScenario;
+import org.apache.uima.cas.serdes.scenario.SerDesTestScenario;
+import org.apache.uima.cas.serdes.scenario.SerRefTestScenario;
+import org.apache.uima.cas.serdes.transitions.CasDesSerCycleConfiguration;
+import org.apache.uima.cas.serdes.transitions.CasSerDesCycleConfiguration;
+import org.apache.uima.json.jsoncas2.mode.FeatureStructuresMode;
+import org.apache.uima.json.jsoncas2.mode.OffsetConversionMode;
+import org.apache.uima.json.jsoncas2.mode.SofaMode;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class CasSerializationDeserialization_JsonCas2_FsAsObject_Test {
+
+  private static final String CAS_FILE_NAME = "data.json";
+  private static final int RANDOM_CAS_ITERATIONS = 20;
+
+  private static final List<CasSerDesCycleConfiguration> serDesCycles = asList( //
+          new CasSerDesCycleConfiguration("DEFAULT (default offsets)", //
+                  (a, b) -> serdes(a, b, null)),
+          new CasSerDesCycleConfiguration("DEFAULT (UTF-16 offsets)", //
+                  (a, b) -> serdes(a, b, OffsetConversionMode.UTF_16)),
+          new CasSerDesCycleConfiguration("DEFAULT (UTF-8 offsets)", //
+                  (a, b) -> serdes(a, b, OffsetConversionMode.UTF_8)),
+          new CasSerDesCycleConfiguration("DEFAULT (UTF-32 offsets)", //
+                  (a, b) -> serdes(a, b, OffsetConversionMode.UTF_32)));
+  // new CasSerDesCycleConfiguration(FORMAT + " / LENIENT", //
+  // (a, b) -> serdes(a, b, FORMAT, LENIENT)));
+
+  private static final List<CasDesSerCycleConfiguration> desSerCycles = asList( //
+          new CasDesSerCycleConfiguration("DEFAULT", //
+                  (a, b) -> desser(createCasMaybeWithTypesystem(a), a, b)));
+
+  private static void ser(CAS aSourceCas, Path aTargetCasFile) throws IOException {
+    JsonCas2Serializer serializer = new JsonCas2Serializer();
+    serializer.setFsMode(FeatureStructuresMode.AS_OBJECT);
+    serializer.setSofaMode(SofaMode.AS_REGULAR_FEATURE_STRUCTURE);
+    serializer.serialize(aSourceCas, aTargetCasFile.toFile());
+  }
+
+  private static void des(CAS aTargetCas, Path aSourceCasFile) throws IOException {
+    JsonCas2Deserializer deserializer = new JsonCas2Deserializer();
+    deserializer.setFsMode(FeatureStructuresMode.AS_OBJECT);
+    deserializer.deserialize(aSourceCasFile.toFile(), aTargetCas);
+  }
+
+  public static void serdes(CAS aSourceCas, CAS aTargetCas, OffsetConversionMode aOcm)
+          throws IOException {
+    byte[] buffer;
+    try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+      JsonCas2Serializer serializer = new JsonCas2Serializer();
+      serializer.setFsMode(FeatureStructuresMode.AS_OBJECT);
+      serializer.setSofaMode(SofaMode.AS_REGULAR_FEATURE_STRUCTURE);
+      serializer.setOffsetConversionMode(aOcm);
+      serializer.serialize(aSourceCas, os);
+      buffer = os.toByteArray();
+    }
+
+    Path targetFile = SER_DES.getTargetFolder(lookup().lookupClass()).resolve(CAS_FILE_NAME);
+    Files.createDirectories(targetFile.getParent());
+    try (OutputStream os = Files.newOutputStream(targetFile)) {
+      os.write(buffer);
+    }
+
+    try (InputStream is = new ByteArrayInputStream(buffer)) {
+      JsonCas2Deserializer deserializer = new JsonCas2Deserializer();
+      deserializer.setFsMode(FeatureStructuresMode.AS_OBJECT);
+      deserializer.deserialize(is, aTargetCas);
+    }
+  }
+
+  public static void desser(CAS aBufferCas, Path aSourceCasPath, Path aTargetCasPath)
+          throws Exception {
+    des(aBufferCas, aSourceCasPath);
+    ser(aBufferCas, aTargetCasPath);
+  }
+
+  private static List<SerRefTestScenario> serRefScenarios() {
+    Class<?> caller = CasSerializationDeserialization_JsonCas2_FsAsObject_Test.class;
+    return ProgrammaticallyCreatedCasDataSuite.builder().build().stream()
+            .map(conf -> SerRefTestScenario.builder(caller, conf, SER_REF, CAS_FILE_NAME)
+                    .withSerializer((cas, path) -> ser(cas, path)).build())
+            .collect(toList());
+  }
+
+  private static List<SerRefTestScenario> oneWayDesSerScenarios() throws Exception {
+    Class<?> caller = CasSerializationDeserialization_JsonCas2_FsAsObject_Test.class;
+    return XmiFileDataSuite
+            .configurations(Paths.get("..", "uimaj-core").resolve(XMI_SUITE_BASE_PATH)).stream()
+            .map(conf -> SerRefTestScenario.builder(caller, conf, ONE_WAY, CAS_FILE_NAME)
+                    .withSerializer((cas, path) -> ser(cas, path)).build())
+            .collect(toList());
+  }
+
+  private static List<DesSerTestScenario> roundTripDesSerScenarios() throws Exception {
+    return SerDesCasIOTestUtils.roundTripDesSerScenariosComparingFileContents(desSerCycles,
+            CAS_FILE_NAME);
+  }
+
+  private static List<SerDesTestScenario> serDesScenarios() {
+    return SerDesCasIOTestUtils.programmaticSerDesScenarios(serDesCycles);
+  }
+
+  private static List<SerDesTestScenario> randomSerDesScenarios() {
+    return SerDesCasIOTestUtils.serDesScenarios(serDesCycles,
+            MultiFeatureRandomCasDataSuite.builder().withIterations(RANDOM_CAS_ITERATIONS).build(),
+            MultiTypeRandomCasDataSuite.builder().withIterations(RANDOM_CAS_ITERATIONS).build());
+  }
+
+  @ParameterizedTest
+  @MethodSource("serRefScenarios")
+  public void serializeAndCompareToReferenceTest(Runnable aScenario) throws Exception {
+    aScenario.run();
+  }
+
+  @ParameterizedTest
+  @MethodSource("oneWayDesSerScenarios")
+  public void oneWayDeserializeSerializeTest(Runnable aScenario) throws Exception {
+    aScenario.run();
+  }
+
+  @ParameterizedTest
+  @MethodSource("serDesScenarios")
+  public void serializeDeserializeTest(Runnable aScenario) throws Exception {
+    aScenario.run();
+  }
+
+  @ParameterizedTest
+  @MethodSource("randomSerDesScenarios")
+  public void randomizedSerializeDeserializeTest(Runnable aScenario) throws Exception {
+    aScenario.run();
+  }
+
+  @ParameterizedTest
+  @MethodSource("roundTripDesSerScenarios")
+  public void roundTripDeserializeSerializeTest(Runnable aScenario) throws Exception {
+    aScenario.run();
+  }
+}
diff --git a/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/Performance_JsonCas2_FsAsArray_Test.java b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/Performance_JsonCas2_FsAsArray_Test.java
new file mode 100644
index 0000000..eb92976
--- /dev/null
+++ b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/Performance_JsonCas2_FsAsArray_Test.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2;
+
+import java.util.Random;
+
+import org.apache.uima.cas.serdes.PerformanceTestRunner;
+import org.apache.uima.cas.serdes.generators.MultiFeatureRandomCasGenerator;
+import org.apache.uima.json.jsoncas2.mode.FeatureStructuresMode;
+import org.apache.uima.json.jsoncas2.mode.SofaMode;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class Performance_JsonCas2_FsAsArray_Test {
+
+  private static final int ITERATIONS = 100;
+  private static final int SIZE = 1_000;
+
+  private static JsonCas2Serializer jsonSerializer;
+  private static JsonCas2Deserializer jsonDeserializer;
+
+  @BeforeAll
+  public static void setup() throws Exception {
+    jsonSerializer = new JsonCas2Serializer();
+    jsonSerializer.setFsMode(FeatureStructuresMode.AS_ARRAY);
+    jsonSerializer.setSofaMode(SofaMode.AS_REGULAR_FEATURE_STRUCTURE);
+
+    jsonDeserializer = new JsonCas2Deserializer();
+    jsonDeserializer.setFsMode(jsonSerializer.getFsMode());
+  }
+
+  @Test
+  public void jsonSerialization() throws Exception {
+    PerformanceTestRunner runner = buildRunner();
+    long serDuration = runner.measureSerializationPerformance();
+
+    System.out.printf("[%23s] %d CASes with %d feature structures (%7d bytes each)%n", "JSON",
+            ITERATIONS, SIZE, runner.getDataSize());
+
+    System.out.printf("[%23s]   %6s ms serialization    %6.2f fs/sec  %6.2f CAS/sec %n", "JSON",
+            serDuration, (ITERATIONS * SIZE) / (serDuration / 1000.0d),
+            ITERATIONS / (serDuration / 1000.0d));
+
+    long desDuration = runner.measureDeserializationPerformance();
+
+    System.out.printf("[%23s]   %6s ms deserialization  %6.2f fs/sec  %6.2f CAS/sec %n", "JSON",
+            desDuration, (ITERATIONS * SIZE) / (desDuration / 1000.0d),
+            ITERATIONS / (desDuration / 1000.0d));
+  }
+
+  public PerformanceTestRunner buildRunner() throws Exception {
+    return PerformanceTestRunner.builder() //
+            .withIterations(ITERATIONS) //
+            .withDeserializer(jsonDeserializer::deserialize)
+            .withSerializer(jsonSerializer::serialize) //
+            .withGenerator(MultiFeatureRandomCasGenerator.builder() //
+                    .withRandomGenerator(new Random(123456l)) //
+                    .withSize(SIZE) //
+                    .build()) //
+            .build();
+  }
+}
diff --git a/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/encoding/Utf32CodepointOffsetConverterTest.java b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/encoding/Utf32CodepointOffsetConverterTest.java
new file mode 100644
index 0000000..171f5d9
--- /dev/null
+++ b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/encoding/Utf32CodepointOffsetConverterTest.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.encoding;
+
+import static java.lang.Character.charCount;
+import static java.lang.Character.getName;
+import static java.lang.Character.toChars;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static net.javacrumbs.jsonunit.jsonpath.JsonPathAdapter.inPath;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.ByteArrayOutputStream;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.serdes.datasuites.ProgrammaticallyCreatedCasDataSuite;
+import org.apache.uima.cas.text.AnnotationFS;
+import org.apache.uima.json.jsoncas2.JsonCas2Serializer;
+import org.apache.uima.json.jsoncas2.mode.OffsetConversionMode;
+import org.junit.jupiter.api.Test;
+
+import net.javacrumbs.jsonunit.assertj.JsonAssertions;
+
+public class Utf32CodepointOffsetConverterTest {
+  @Test
+  public void thatMappingExternalToInternalWorks() {
+    int[] externalToInternal = {
+        // Smily
+        0,
+        // " This "
+        2, 3, 4, 5, 6, 7,
+        // Female light-skinned turban emoji
+        8, 10, 12, 13, 14,
+        // " is "
+        15, 16, 17, 18,
+        // Telephone sign
+        19,
+        // " a "
+        20, 21, 22,
+        // Dark skinned bearded emoji
+        23, 25, 27, 28, 29,
+        // " test "
+        30, 31, 32, 33, 34, 35,
+        // Ghost emoji
+        36,
+        // EOS
+        38 };
+    String text = "🥳 This 👳🏻‍♀️ is ✆ a 🧔🏾‍♂️ test 👻";
+    Utf32CodepointOffsetConverter conv = new Utf32CodepointOffsetConverter(text);
+    for (int n = 0; n < externalToInternal.length; n++) {
+      int mn = externalToInternal[n];
+      if (mn < text.length()) {
+        int cp = text.codePointAt(mn);
+        int w = charCount(cp);
+        System.out.printf("%2d->%2d: U+%05X (%d) %s %s%n", n, mn, cp, w,
+                String.valueOf(toChars(cp)), getName(cp));
+      } else {
+        System.out.printf("%2d->%2d: EOS%n", n, mn);
+      }
+      assertThat(conv.mapExternal(n)).isEqualTo(mn);
+    }
+  }
+
+  @Test
+  public void thatMappingInternalToExternalWorks() {
+    final int U = Utf32CodepointOffsetConverter.UNMAPPED;
+    int[] internalToExternal = {
+        // Smily
+        0, U,
+        // " This "
+        1, 2, 3, 4, 5, 6,
+        // Female light-skinned turban emoji
+        7, U, 8, U, 9, 10, 11,
+        // " is "
+        12, 13, 14, 15,
+        // Telephone sign
+        16,
+        // " a "
+        17, 18, 19,
+        // Dark skinned bearded emoji
+        20, U, 21, U, 22, 23, 24,
+        // " test "
+        25, 26, 27, 28, 29, 30,
+        // Ghost emoji
+        31, U,
+        // EOS
+        32 };
+    String text = "🥳 This 👳🏻‍♀️ is ✆ a 🧔🏾‍♂️ test 👻";
+    int cpl = text.codePointCount(0, text.length());
+    Utf32CodepointOffsetConverter conv = new Utf32CodepointOffsetConverter(text);
+    for (int n = 0; n < internalToExternal.length; n++) {
+      int mn = internalToExternal[n];
+      if (mn < cpl) {
+        if (mn != Utf32CodepointOffsetConverter.UNMAPPED) {
+          int cp = text.codePointAt(n);
+          int w = Character.charCount(cp);
+          // System.out.printf("%2d->%2d: U+%05X (%d) %s %s%n", n, mn, cp, w,
+          // String.valueOf(toChars(cp)), getName(cp));
+        } else {
+          // System.out.printf("%2d->%2d: EOS%n", n, mn);
+        }
+      }
+
+      assertThat(conv.mapInternal(n)).isEqualTo(mn);
+    }
+  }
+
+  @Test
+  public void thatSerializationWithMappingWorks() throws Exception {
+    JsonCas2Serializer ser = new JsonCas2Serializer();
+    ser.setOffsetConversionMode(OffsetConversionMode.UTF_32);
+
+    CAS cas = ProgrammaticallyCreatedCasDataSuite.casWithEmojiUnicodeTextAndAnnotations();
+
+    String casJson;
+    try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+      ser.serialize(cas, os);
+      casJson = new String(os.toByteArray(), UTF_8);
+    }
+
+    String expected = String.join(",",
+            "{'%ID':1,'%TYPE':'uima.cas.Sofa','sofaNum':1,'sofaID':'_InitialView','mimeType':'text','sofaString':'🥳 This 👳🏻‍♀️ is ✆ a 🧔🏾‍♂️ test 👻'}",
+            "{'%ID':2,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':0,'end':1}",
+            "{'%ID':3,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':2,'end':6}",
+            "{'%ID':4,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':7,'end':12}",
+            "{'%ID':5,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':13,'end':15}",
+            "{'%ID':6,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':16,'end':17}",
+            "{'%ID':7,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':18,'end':19}",
+            "{'%ID':8,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':20,'end':25}",
+            "{'%ID':9,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':26,'end':30}",
+            "{'%ID':10,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':31,'end':32}",
+            "{'%ID':11,'%TYPE':'uima.tcas.DocumentAnnotation','@sofa':1,'begin':0,'end':32,'language':'x-unspecified'}");
+    expected = "[" + expected + "]";
+
+    JsonAssertions.assertThatJson(inPath(casJson, "$.%FEATURE_STRUCTURES[*])")) //
+            .isEqualTo(expected);
+  }
+
+  private static void createAnnotatedText(CAS aCas, StringBuilder aBuffer, String aText,
+          String... aSuffix) {
+    int begin = aBuffer.length();
+    aBuffer.append(aText);
+    AnnotationFS a = aCas.createAnnotation(aCas.getAnnotationType(), begin, aBuffer.length());
+    aCas.addFsToIndexes(a);
+    for (String s : aSuffix) {
+      aBuffer.append(s);
+    }
+  }
+}
diff --git a/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/encoding/Utf8ByteOffsetConverterTest.java b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/encoding/Utf8ByteOffsetConverterTest.java
new file mode 100644
index 0000000..8ca966f
--- /dev/null
+++ b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/encoding/Utf8ByteOffsetConverterTest.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.encoding;
+
+import static java.lang.Character.getName;
+import static java.lang.Character.toChars;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static net.javacrumbs.jsonunit.jsonpath.JsonPathAdapter.inPath;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.ByteArrayOutputStream;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.serdes.datasuites.ProgrammaticallyCreatedCasDataSuite;
+import org.apache.uima.cas.text.AnnotationFS;
+import org.apache.uima.json.jsoncas2.JsonCas2Serializer;
+import org.apache.uima.json.jsoncas2.mode.OffsetConversionMode;
+import org.junit.jupiter.api.Test;
+
+import net.javacrumbs.jsonunit.assertj.JsonAssertions;
+
+public class Utf8ByteOffsetConverterTest {
+  @Test
+  public void thatMappingExternalToInternalWorks() {
+    final int U = Utf8ByteOffsetConverter.UNMAPPED;
+    int[] externalToInternal = {
+        // Smily
+        0, U, U, U,
+        // " This "
+        2, 3, 4, 5, 6, 7,
+        // Female light-skinned turban emoji
+        8, U, U, U, 10, U, U, U, 12, U, U, 13, U, U, 14, U, U,
+        // " is "
+        15, 16, 17, 18,
+        // Telephone sign
+        19, U, U,
+        // " a "
+        20, 21, 22,
+        // Dark skinned bearded emoji
+        23, U, U, U, 25, U, U, U, 27, U, U, 28, U, U, 29, U, U,
+        // " test "
+        30, 31, 32, 33, 34, 35,
+        // Ghost emoji
+        36, U, U, U,
+        // EOS
+        38 };
+    String text = "🥳 This 👳🏻‍♀️ is ✆ a 🧔🏾‍♂️ test 👻";
+    int external_length = text.getBytes(UTF_8).length;
+    Utf8ByteOffsetConverter conv = new Utf8ByteOffsetConverter(text);
+    for (int n = 0; n < externalToInternal.length; n++) {
+      int mn = externalToInternal[n];
+      if (n < external_length) {
+        if (mn != Utf8ByteOffsetConverter.UNMAPPED) {
+          int cp = text.codePointAt(mn);
+          String s = String.valueOf(toChars(cp));
+          int w = s.getBytes(UTF_8).length;
+          System.out.printf("%2d->%2d: U+%05X (%d) %s %s%n", n, mn, cp, w, s, getName(cp));
+        }
+      } else {
+        System.out.printf("%2d->%2d: EOS%n", n, mn);
+      }
+
+      assertThat(conv.mapExternal(n)).isEqualTo(mn);
+    }
+  }
+
+  @Test
+  public void thatMappingInternalToExternalWorks() {
+    final int U = Utf8ByteOffsetConverter.UNMAPPED;
+    int[] internalToExternal = {
+        // Smily
+        0, U,
+        // " This "
+        4, 5, 6, 7, 8, 9,
+        // Female light-skinned turban emoji
+        10, U, 14, U, 18, 21, 24,
+        // " is "
+        27, 28, 29, 30,
+        // Telephone sign
+        31,
+        // " a "
+        34, 35, 36,
+        // Dark skinned bearded emoji
+        37, U, 41, U, 45, 48, 51,
+        // " test "
+        54, 55, 56, 57, 58, 59,
+        // Ghost emoji
+        60, U,
+        // EOS
+        64 };
+    String text = "🥳 This 👳🏻‍♀️ is ✆ a 🧔🏾‍♂️ test 👻";
+    int cul = text.length();
+    Utf8ByteOffsetConverter conv = new Utf8ByteOffsetConverter(text);
+    for (int n = 0; n < internalToExternal.length; n++) {
+      int mn = internalToExternal[n];
+      if (n < cul) {
+        if (mn != Utf8ByteOffsetConverter.UNMAPPED) {
+          int cp = text.codePointAt(n);
+          String s = String.valueOf(toChars(cp));
+          int w = String.valueOf(Character.toChars(cp)).getBytes(UTF_8).length;
+          // System.out.printf("%2d->%2d: U+%05X (%d) %s %s%n", n, mn, cp, w, s, getName(cp));
+        }
+      } else {
+        // System.out.printf("%2d->%2d: EOS%n", n, mn);
+      }
+
+      assertThat(conv.mapInternal(n)).isEqualTo(mn);
+    }
+  }
+
+  @Test
+  public void thatSerializationWithMappingWorks() throws Exception {
+    JsonCas2Serializer ser = new JsonCas2Serializer();
+    ser.setOffsetConversionMode(OffsetConversionMode.UTF_8);
+
+    CAS cas = ProgrammaticallyCreatedCasDataSuite.casWithEmojiUnicodeTextAndAnnotations();
+
+    String casJson;
+    try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+      ser.serialize(cas, os);
+      casJson = new String(os.toByteArray(), UTF_8);
+    }
+
+    String expected = String.join(",",
+            "{'%ID':1,'%TYPE':'uima.cas.Sofa','sofaNum':1,'sofaID':'_InitialView','mimeType':'text','sofaString':'🥳 This 👳🏻‍♀️ is ✆ a 🧔🏾‍♂️ test 👻'}",
+            "{'%ID':2,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':0,'end':4}",
+            "{'%ID':3,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':5,'end':9}",
+            "{'%ID':4,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':10,'end':27}",
+            "{'%ID':5,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':28,'end':30}",
+            "{'%ID':6,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':31,'end':34}",
+            "{'%ID':7,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':35,'end':36}",
+            "{'%ID':8,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':37,'end':54}",
+            "{'%ID':9,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':55,'end':59}",
+            "{'%ID':10,'%TYPE':'uima.tcas.Annotation','@sofa':1,'begin':60,'end':64}",
+            "{'%ID':11,'%TYPE':'uima.tcas.DocumentAnnotation','@sofa':1,'begin':0,'end':64,'language':'x-unspecified'}");
+    expected = "[" + expected + "]";
+
+    JsonAssertions.assertThatJson(inPath(casJson, "$.%FEATURE_STRUCTURES[*])")) //
+            .isEqualTo(expected);
+  }
+
+  private static void createAnnotatedText(CAS aCas, StringBuilder aBuffer, String aText,
+          String... aSuffix) {
+    int begin = aBuffer.length();
+    aBuffer.append(aText);
+    AnnotationFS a = aCas.createAnnotation(aCas.getAnnotationType(), begin, aBuffer.length());
+    aCas.addFsToIndexes(a);
+    for (String s : aSuffix) {
+      aBuffer.append(s);
+    }
+  }
+}
diff --git a/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/ser/FeatureDeSerializerTest.java b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/ser/FeatureDeSerializerTest.java
new file mode 100644
index 0000000..e144757
--- /dev/null
+++ b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/ser/FeatureDeSerializerTest.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static org.apache.uima.cas.CAS.TYPE_NAME_ANNOTATION;
+import static org.apache.uima.cas.CAS.TYPE_NAME_FS_ARRAY;
+import static org.apache.uima.cas.CAS.TYPE_NAME_FS_LIST;
+import static org.apache.uima.cas.CAS.TYPE_NAME_INTEGER;
+import static org.apache.uima.cas.CAS.TYPE_NAME_INTEGER_ARRAY;
+import static org.apache.uima.cas.CAS.TYPE_NAME_STRING;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.StringWriter;
+
+import org.apache.uima.UIMAFramework;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Feature;
+import org.apache.uima.cas.TypeSystem;
+import org.apache.uima.json.jsoncas2.ref.ReferenceCache;
+import org.apache.uima.resource.metadata.FeatureDescription;
+import org.apache.uima.resource.metadata.TypeDescription;
+import org.apache.uima.resource.metadata.TypeSystemDescription;
+import org.apache.uima.util.CasCreationUtils;
+import org.apache.uima.util.TypeSystemUtil;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+public class FeatureDeSerializerTest {
+  private Logger log = LoggerFactory.getLogger(getClass());
+
+  private static final String TYPE = "Type";
+  private static final String FEATURE = "feature";
+
+  private TypeSystemDescription tsd;
+  private TypeDescription td;
+
+  @BeforeEach
+  public void setup() {
+    tsd = UIMAFramework.getResourceSpecifierFactory().createTypeSystemDescription();
+    td = tsd.addType(TYPE, null, CAS.TYPE_NAME_ANNOTATION);
+  }
+
+  @Test
+  public void testIntegerFeature() throws Exception {
+    FeatureDescription expected = td.addFeature(FEATURE, null, TYPE_NAME_INTEGER);
+    FeatureDescription actual = serdes(tsd);
+
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testStringFeature() throws Exception {
+    FeatureDescription expected = td.addFeature(FEATURE, null, TYPE_NAME_STRING);
+    FeatureDescription actual = serdes(tsd);
+
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testFSArrayFeature() throws Exception {
+    FeatureDescription expected = td.addFeature(FEATURE, null, TYPE_NAME_FS_ARRAY,
+            TYPE_NAME_ANNOTATION, null);
+    FeatureDescription actual = serdes(tsd);
+
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testIntegerArrayFeature() throws Exception {
+    FeatureDescription expected = td.addFeature(FEATURE, null, TYPE_NAME_INTEGER_ARRAY,
+            TYPE_NAME_INTEGER, null);
+    FeatureDescription actual = serdes(tsd);
+
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testFSListFeature() throws Exception {
+    FeatureDescription expected = td.addFeature(FEATURE, null, TYPE_NAME_FS_LIST,
+            TYPE_NAME_ANNOTATION, null);
+    FeatureDescription actual = serdes(tsd);
+
+    // HACK: UIMA does not preserve the element type for FSList features!!! Bug?
+    expected.setElementType(null);
+
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  private FeatureDescription serdes(TypeSystemDescription aTsd) throws Exception {
+    ObjectMapper mapper = getMapper();
+    CAS cas = CasCreationUtils.createCas(aTsd, null, null, null);
+    TypeSystem ts = cas.getTypeSystem();
+
+    StringWriter buf = new StringWriter();
+    TypeSystemUtil.type2TypeDescription(ts.getType(TYPE), ts).toXML(buf);
+
+    String json = mapper.writer() //
+            .withAttribute(ReferenceCache.KEY, ReferenceCache.builder().build()) //
+            .writeValueAsString(ts.getType(TYPE).getFeatureByBaseName(FEATURE));
+
+    FeatureDescription fdActual = mapper.reader() //
+            .forType(FeatureDescription.class) //
+            .readValue(json);
+
+    return fdActual;
+  }
+
+  private ObjectMapper getMapper() {
+    SimpleModule module = new SimpleModule("UIMA CAS JSON", new Version(1, 0, 0, null, null, null));
+
+    module.addSerializer(Feature.class, new FeatureSerializer());
+    module.addDeserializer(FeatureDescription.class, new FeatureDeserializer());
+
+    ObjectMapper mapper = new ObjectMapper();
+    mapper.registerModule(module);
+    return mapper;
+  }
+}
diff --git a/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/ser/TypeDeSerializerTest.java b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/ser/TypeDeSerializerTest.java
new file mode 100644
index 0000000..7e97998
--- /dev/null
+++ b/uimaj-json/src/test/java/org/apache/uima/json/jsoncas2/ser/TypeDeSerializerTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.uima.json.jsoncas2.ser;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.uima.UIMAFramework;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Feature;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.TypeSystem;
+import org.apache.uima.json.jsoncas2.ref.ReferenceCache;
+import org.apache.uima.resource.metadata.FeatureDescription;
+import org.apache.uima.resource.metadata.TypeDescription;
+import org.apache.uima.resource.metadata.TypeSystemDescription;
+import org.apache.uima.util.CasCreationUtils;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+public class TypeDeSerializerTest {
+  private Logger log = LoggerFactory.getLogger(getClass());
+
+  @Test
+  public void thatTypeDeSerializationWorks() throws Exception {
+    ObjectMapper mapper = getMapper();
+
+    TypeSystemDescription tsd = UIMAFramework.getResourceSpecifierFactory()
+            .createTypeSystemDescription();
+    TypeDescription tdExpected = tsd.addType("Type1", null, CAS.TYPE_NAME_ANNOTATION);
+
+    TypeSystem ts = CasCreationUtils.createCas(tsd, null, null, null).getTypeSystem();
+
+    String json = mapper.writer() //
+            .withAttribute(ReferenceCache.KEY, ReferenceCache.builder().build()) //
+            .writeValueAsString(ts.getType("Type1"));
+
+    TypeDescription tdActual = mapper.reader() //
+            .forType(TypeDescription.class) //
+            .readValue(json);
+
+    assertThat(tdActual).isEqualTo(tdExpected);
+  }
+
+  private ObjectMapper getMapper() {
+    SimpleModule module = new SimpleModule("UIMA CAS JSON", new Version(1, 0, 0, null, null, null));
+
+    module.addSerializer(Type.class, new TypeSerializer());
+    module.addDeserializer(TypeDescription.class, new TypeDeserializer());
+    module.addSerializer(Feature.class, new FeatureSerializer());
+    module.addDeserializer(FeatureDescription.class, new FeatureDeserializer());
+
+    ObjectMapper mapper = new ObjectMapper();
+    mapper.registerModule(module);
+    return mapper;
+  }
+}
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/data.json
new file mode 100644
index 0000000..422cea5
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/data.json
@@ -0,0 +1,78 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : [ {
+    "%ID" : 1,
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 1,
+    "sofaID" : "_InitialView",
+    "mimeType" : "text",
+    "sofaString" : "\uD83E\uDD73 This \uD83D\uDC73\uD83C\uDFFB‍♀️ is ✆ a \uD83E\uDDD4\uD83C\uDFFE‍♂️ test \uD83D\uDC7B"
+  }, {
+    "%ID" : 2,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 0,
+    "end" : 2
+  }, {
+    "%ID" : 3,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 3,
+    "end" : 7
+  }, {
+    "%ID" : 4,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 8,
+    "end" : 15
+  }, {
+    "%ID" : 5,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 16,
+    "end" : 18
+  }, {
+    "%ID" : 6,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 19,
+    "end" : 20
+  }, {
+    "%ID" : 7,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 21,
+    "end" : 22
+  }, {
+    "%ID" : 8,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 23,
+    "end" : 30
+  }, {
+    "%ID" : 9,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 31,
+    "end" : 35
+  }, {
+    "%ID" : 10,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 36,
+    "end" : 38
+  }, {
+    "%ID" : 11,
+    "%TYPE" : "uima.tcas.DocumentAnnotation",
+    "@sofa" : 1,
+    "begin" : 0,
+    "end" : 38,
+    "language" : "x-unspecified"
+  } ],
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/debug.xmi
new file mode 100644
index 0000000..6d8ec43
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/debug.xmi
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <tcas:Annotation xmi:id="2" sofa="1" begin="0" end="2"/>
+    <tcas:Annotation xmi:id="3" sofa="1" begin="3" end="7"/>
+    <tcas:Annotation xmi:id="4" sofa="1" begin="8" end="15"/>
+    <tcas:Annotation xmi:id="5" sofa="1" begin="16" end="18"/>
+    <tcas:Annotation xmi:id="6" sofa="1" begin="19" end="20"/>
+    <tcas:Annotation xmi:id="7" sofa="1" begin="21" end="22"/>
+    <tcas:Annotation xmi:id="8" sofa="1" begin="23" end="30"/>
+    <tcas:Annotation xmi:id="9" sofa="1" begin="31" end="35"/>
+    <tcas:Annotation xmi:id="10" sofa="1" begin="36" end="38"/>
+    <tcas:DocumentAnnotation xmi:id="11" sofa="1" begin="0" end="38" language="x-unspecified"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView" mimeType="text" sofaString="&#129395; This &#128115;&#127995;‍♀️ is ✆ a &#129492;&#127998;‍♂️ test &#128123;"/>
+    <cas:View sofa="1" members="2 3 4 5 6 7 8 9 10 11"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithFloatingPointSpecialValues/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithFloatingPointSpecialValues/data.json
new file mode 100644
index 0000000..206f7d4
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithFloatingPointSpecialValues/data.json
@@ -0,0 +1,76 @@
+{
+  "%TYPES" : {
+    "SpecialValuesType" : {
+      "%NAME" : "SpecialValuesType",
+      "%SUPER_TYPE" : "uima.cas.TOP",
+      "doubleZero" : {
+        "%NAME" : "doubleZero",
+        "%RANGE" : "uima.cas.Double"
+      },
+      "doubleOne" : {
+        "%NAME" : "doubleOne",
+        "%RANGE" : "uima.cas.Double"
+      },
+      "doublePosInfinity" : {
+        "%NAME" : "doublePosInfinity",
+        "%RANGE" : "uima.cas.Double"
+      },
+      "doubleNegInfinity" : {
+        "%NAME" : "doubleNegInfinity",
+        "%RANGE" : "uima.cas.Double"
+      },
+      "doubleNan" : {
+        "%NAME" : "doubleNan",
+        "%RANGE" : "uima.cas.Double"
+      },
+      "floatZero" : {
+        "%NAME" : "floatZero",
+        "%RANGE" : "uima.cas.Float"
+      },
+      "floatOne" : {
+        "%NAME" : "floatOne",
+        "%RANGE" : "uima.cas.Float"
+      },
+      "floatPosInfinity" : {
+        "%NAME" : "floatPosInfinity",
+        "%RANGE" : "uima.cas.Float"
+      },
+      "floatNegInfinity" : {
+        "%NAME" : "floatNegInfinity",
+        "%RANGE" : "uima.cas.Float"
+      },
+      "floatNan" : {
+        "%NAME" : "floatNan",
+        "%RANGE" : "uima.cas.Float"
+      }
+    }
+  },
+  "%FEATURE_STRUCTURES" : [ {
+    "%ID" : 1,
+    "%TYPE" : "SpecialValuesType",
+    "doubleZero" : 0.0,
+    "doubleOne" : 1.0,
+    "#doublePosInfinity" : "Infinity",
+    "#doubleNegInfinity" : "-Infinity",
+    "#doubleNan" : "NaN",
+    "floatZero" : 0.0,
+    "floatOne" : 1.0,
+    "#floatPosInfinity" : "Infinity",
+    "#floatNegInfinity" : "-Infinity",
+    "#floatNan" : "NaN"
+  }, {
+    "%ID" : 2,
+    "%TYPE" : "uima.cas.DoubleArray",
+    "%ELEMENTS" : [ 0.0, 1.0, "-Infinity", "Infinity", "NaN" ]
+  }, {
+    "%ID" : 3,
+    "%TYPE" : "uima.cas.FloatArray",
+    "%ELEMENTS" : [ 0.0, 1.0, "-Infinity", "Infinity", "NaN" ]
+  } ],
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 4,
+      "%MEMBERS" : [ 1, 2, 3 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithFloatingPointSpecialValues/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithFloatingPointSpecialValues/debug-typesystem.xml
new file mode 100644
index 0000000..9a8766d
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithFloatingPointSpecialValues/debug-typesystem.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+        <typeDescription>
+            <name>SpecialValuesType</name>
+            <description/>
+            <supertypeName>uima.cas.TOP</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>doubleZero</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doubleOne</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doublePosInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doubleNegInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doubleNan</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatZero</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatOne</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatPosInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatNegInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatNan</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithFloatingPointSpecialValues/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithFloatingPointSpecialValues/debug.xmi
new file mode 100644
index 0000000..e02d4cb
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithFloatingPointSpecialValues/debug.xmi
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:noNamespace="http:///uima/noNamespace.ecore" xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <noNamespace:SpecialValuesType xmi:id="1" doubleZero="0.0" doubleOne="1.0" doublePosInfinity="Infinity" doubleNegInfinity="-Infinity" doubleNan="NaN" floatZero="0.0" floatOne="1.0" floatPosInfinity="Infinity" floatNegInfinity="-Infinity" floatNan="NaN"/>
+    <cas:DoubleArray xmi:id="2" elements="0.0 1.0 -Infinity Infinity NaN"/>
+    <cas:FloatArray xmi:id="3" elements="0.0 1.0 -Infinity Infinity NaN"/>
+    <cas:View members="1 2 3"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithFloatingPointSpecialValues/typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithFloatingPointSpecialValues/typesystem.xml
new file mode 100644
index 0000000..9a8766d
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithFloatingPointSpecialValues/typesystem.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+        <typeDescription>
+            <name>SpecialValuesType</name>
+            <description/>
+            <supertypeName>uima.cas.TOP</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>doubleZero</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doubleOne</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doublePosInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doubleNegInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doubleNan</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatZero</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatOne</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatPosInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatNegInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatNan</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithLeftToRightTextAndAnnotations/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithLeftToRightTextAndAnnotations/data.json
new file mode 100644
index 0000000..1944181
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithLeftToRightTextAndAnnotations/data.json
@@ -0,0 +1,36 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : [ {
+    "%ID" : 1,
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 1,
+    "sofaID" : "_InitialView",
+    "mimeType" : "text",
+    "sofaString" : "هذا اختبار"
+  }, {
+    "%ID" : 2,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 0,
+    "end" : 3
+  }, {
+    "%ID" : 3,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 4,
+    "end" : 10
+  }, {
+    "%ID" : 4,
+    "%TYPE" : "uima.tcas.DocumentAnnotation",
+    "@sofa" : 1,
+    "begin" : 0,
+    "end" : 10,
+    "language" : "x-unspecified"
+  } ],
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ 2, 3, 4 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithLeftToRightTextAndAnnotations/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithLeftToRightTextAndAnnotations/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithLeftToRightTextAndAnnotations/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithLeftToRightTextAndAnnotations/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithLeftToRightTextAndAnnotations/debug.xmi
new file mode 100644
index 0000000..108d362
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithLeftToRightTextAndAnnotations/debug.xmi
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <tcas:Annotation xmi:id="2" sofa="1" begin="0" end="3"/>
+    <tcas:Annotation xmi:id="3" sofa="1" begin="4" end="10"/>
+    <tcas:DocumentAnnotation xmi:id="4" sofa="1" begin="0" end="10" language="x-unspecified"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView" mimeType="text" sofaString="هذا اختبار"/>
+    <cas:View sofa="1" members="2 3 4"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataArray/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataArray/data.json
new file mode 100644
index 0000000..20d935b
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataArray/data.json
@@ -0,0 +1,21 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : [ {
+    "%ID" : 1,
+    "%TYPE" : "uima.cas.ByteArray",
+    "%ELEMENTS" : "VGhpcyBpcyBhIHRlc3Q="
+  }, {
+    "%ID" : 2,
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 1,
+    "sofaID" : "_InitialView",
+    "mimeType" : "text/plain",
+    "@sofaArray" : 1
+  } ],
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 2,
+      "%MEMBERS" : [ ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataArray/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataArray/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataArray/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataArray/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataArray/debug.xmi
new file mode 100644
index 0000000..89075f6
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataArray/debug.xmi
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <cas:Sofa xmi:id="2" sofaNum="1" sofaID="_InitialView" mimeType="text/plain" sofaArray="1"/>
+    <cas:ByteArray xmi:id="1" elements="5468697320697320612074657374"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataURI/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataURI/data.json
new file mode 100644
index 0000000..0b142a8
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataURI/data.json
@@ -0,0 +1,17 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : [ {
+    "%ID" : 1,
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 1,
+    "sofaID" : "_InitialView",
+    "mimeType" : "text/plain",
+    "sofaURI" : "classpath:/ProgrammaticallyCreatedCasDataSuite/document.txt"
+  } ],
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataURI/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataURI/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataURI/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataURI/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataURI/debug.xmi
new file mode 100644
index 0000000..89966e0
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithSofaDataURI/debug.xmi
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView" mimeType="text/plain" sofaURI="classpath:/ProgrammaticallyCreatedCasDataSuite/document.txt"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithText/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithText/data.json
new file mode 100644
index 0000000..39f5ffe
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithText/data.json
@@ -0,0 +1,24 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : [ {
+    "%ID" : 1,
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 1,
+    "sofaID" : "_InitialView",
+    "mimeType" : "text",
+    "sofaString" : "This is a test."
+  }, {
+    "%ID" : 2,
+    "%TYPE" : "uima.tcas.DocumentAnnotation",
+    "@sofa" : 1,
+    "begin" : 0,
+    "end" : 15,
+    "language" : "x-unspecified"
+  } ],
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ 2 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithText/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithText/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithText/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithText/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithText/debug.xmi
new file mode 100644
index 0000000..943df5f
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithText/debug.xmi
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <tcas:DocumentAnnotation xmi:id="2" sofa="1" begin="0" end="15" language="x-unspecified"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView" mimeType="text" sofaString="This is a test."/>
+    <cas:View sofa="1" members="2"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTextAndAnnotations/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTextAndAnnotations/data.json
new file mode 100644
index 0000000..a9522cf
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTextAndAnnotations/data.json
@@ -0,0 +1,48 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : [ {
+    "%ID" : 1,
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 1,
+    "sofaID" : "_InitialView",
+    "mimeType" : "text",
+    "sofaString" : "This is a test"
+  }, {
+    "%ID" : 2,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 0,
+    "end" : 4
+  }, {
+    "%ID" : 3,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 5,
+    "end" : 7
+  }, {
+    "%ID" : 4,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 8,
+    "end" : 9
+  }, {
+    "%ID" : 5,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 10,
+    "end" : 14
+  }, {
+    "%ID" : 6,
+    "%TYPE" : "uima.tcas.DocumentAnnotation",
+    "@sofa" : 1,
+    "begin" : 0,
+    "end" : 14,
+    "language" : "x-unspecified"
+  } ],
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ 2, 3, 4, 5, 6 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTextAndAnnotations/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTextAndAnnotations/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTextAndAnnotations/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTextAndAnnotations/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTextAndAnnotations/debug.xmi
new file mode 100644
index 0000000..37c1e9b
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTextAndAnnotations/debug.xmi
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <tcas:Annotation xmi:id="2" sofa="1" begin="0" end="4"/>
+    <tcas:Annotation xmi:id="3" sofa="1" begin="5" end="7"/>
+    <tcas:Annotation xmi:id="4" sofa="1" begin="8" end="9"/>
+    <tcas:Annotation xmi:id="5" sofa="1" begin="10" end="14"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView"/>
+    <cas:View sofa="1" members="2 3 4 5"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/data.json
new file mode 100644
index 0000000..d586738
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/data.json
@@ -0,0 +1,48 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : [ {
+    "%ID" : 1,
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 1,
+    "sofaID" : "_InitialView",
+    "mimeType" : "text",
+    "sofaString" : "這是一個測試"
+  }, {
+    "%ID" : 2,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 0,
+    "end" : 1
+  }, {
+    "%ID" : 3,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 1,
+    "end" : 2
+  }, {
+    "%ID" : 4,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 2,
+    "end" : 4
+  }, {
+    "%ID" : 5,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 4,
+    "end" : 6
+  }, {
+    "%ID" : 6,
+    "%TYPE" : "uima.tcas.DocumentAnnotation",
+    "@sofa" : 1,
+    "begin" : 0,
+    "end" : 6,
+    "language" : "x-unspecified"
+  } ],
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ 2, 3, 4, 5, 6 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/debug.xmi
new file mode 100644
index 0000000..0087d72
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/debug.xmi
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <tcas:Annotation xmi:id="2" sofa="1" begin="0" end="1"/>
+    <tcas:Annotation xmi:id="3" sofa="1" begin="1" end="2"/>
+    <tcas:Annotation xmi:id="4" sofa="1" begin="2" end="4"/>
+    <tcas:Annotation xmi:id="5" sofa="1" begin="4" end="6"/>
+    <tcas:DocumentAnnotation xmi:id="6" sofa="1" begin="0" end="6" language="x-unspecified"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView" mimeType="text" sofaString="這是一個測試"/>
+    <cas:View sofa="1" members="2 3 4 5 6"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithoutTextButWithAnnotations/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithoutTextButWithAnnotations/data.json
new file mode 100644
index 0000000..56784fe
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithoutTextButWithAnnotations/data.json
@@ -0,0 +1,39 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : [ {
+    "%ID" : 1,
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 1,
+    "sofaID" : "_InitialView"
+  }, {
+    "%ID" : 2,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 0,
+    "end" : 4
+  }, {
+    "%ID" : 3,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 5,
+    "end" : 7
+  }, {
+    "%ID" : 4,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 8,
+    "end" : 9
+  }, {
+    "%ID" : 5,
+    "%TYPE" : "uima.tcas.Annotation",
+    "@sofa" : 1,
+    "begin" : 10,
+    "end" : 14
+  } ],
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ 2, 3, 4, 5 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithoutTextButWithAnnotations/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithoutTextButWithAnnotations/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithoutTextButWithAnnotations/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithoutTextButWithAnnotations/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithoutTextButWithAnnotations/debug.xmi
new file mode 100644
index 0000000..37c1e9b
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/casWithoutTextButWithAnnotations/debug.xmi
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <tcas:Annotation xmi:id="2" sofa="1" begin="0" end="4"/>
+    <tcas:Annotation xmi:id="3" sofa="1" begin="5" end="7"/>
+    <tcas:Annotation xmi:id="4" sofa="1" begin="8" end="9"/>
+    <tcas:Annotation xmi:id="5" sofa="1" begin="10" end="14"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView"/>
+    <cas:View sofa="1" members="2 3 4 5"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/emptyCas/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/emptyCas/data.json
new file mode 100644
index 0000000..fcd8582
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/emptyCas/data.json
@@ -0,0 +1,9 @@
+{
+  "%TYPES" : { },
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/emptyCas/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/emptyCas/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/emptyCas/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/emptyCas/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/emptyCas/debug.xmi
new file mode 100644
index 0000000..6fd88bd
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsArray_Test/ser-ref/emptyCas/debug.xmi
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/data.json
new file mode 100644
index 0000000..182ef8a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/data.json
@@ -0,0 +1,79 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : {
+    "1" : {
+      "%TYPE" : "uima.cas.Sofa",
+      "sofaNum" : 1,
+      "sofaID" : "_InitialView",
+      "mimeType" : "text",
+      "sofaString" : "\uD83E\uDD73 This \uD83D\uDC73\uD83C\uDFFB‍♀️ is ✆ a \uD83E\uDDD4\uD83C\uDFFE‍♂️ test \uD83D\uDC7B"
+    },
+    "2" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 0,
+      "end" : 2
+    },
+    "3" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 3,
+      "end" : 7
+    },
+    "4" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 8,
+      "end" : 15
+    },
+    "5" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 16,
+      "end" : 18
+    },
+    "6" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 19,
+      "end" : 20
+    },
+    "7" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 21,
+      "end" : 22
+    },
+    "8" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 23,
+      "end" : 30
+    },
+    "9" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 31,
+      "end" : 35
+    },
+    "10" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 36,
+      "end" : 38
+    },
+    "11" : {
+      "%TYPE" : "uima.tcas.DocumentAnnotation",
+      "@sofa" : 1,
+      "begin" : 0,
+      "end" : 38,
+      "language" : "x-unspecified"
+    }
+  },
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/debug.xmi
new file mode 100644
index 0000000..6d8ec43
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithEmojiUnicodeTextAndAnnotations/debug.xmi
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <tcas:Annotation xmi:id="2" sofa="1" begin="0" end="2"/>
+    <tcas:Annotation xmi:id="3" sofa="1" begin="3" end="7"/>
+    <tcas:Annotation xmi:id="4" sofa="1" begin="8" end="15"/>
+    <tcas:Annotation xmi:id="5" sofa="1" begin="16" end="18"/>
+    <tcas:Annotation xmi:id="6" sofa="1" begin="19" end="20"/>
+    <tcas:Annotation xmi:id="7" sofa="1" begin="21" end="22"/>
+    <tcas:Annotation xmi:id="8" sofa="1" begin="23" end="30"/>
+    <tcas:Annotation xmi:id="9" sofa="1" begin="31" end="35"/>
+    <tcas:Annotation xmi:id="10" sofa="1" begin="36" end="38"/>
+    <tcas:DocumentAnnotation xmi:id="11" sofa="1" begin="0" end="38" language="x-unspecified"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView" mimeType="text" sofaString="&#129395; This &#128115;&#127995;‍♀️ is ✆ a &#129492;&#127998;‍♂️ test &#128123;"/>
+    <cas:View sofa="1" members="2 3 4 5 6 7 8 9 10 11"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithFloatingPointSpecialValues/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithFloatingPointSpecialValues/data.json
new file mode 100644
index 0000000..c9028a9
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithFloatingPointSpecialValues/data.json
@@ -0,0 +1,77 @@
+{
+  "%TYPES" : {
+    "SpecialValuesType" : {
+      "%NAME" : "SpecialValuesType",
+      "%SUPER_TYPE" : "uima.cas.TOP",
+      "doubleZero" : {
+        "%NAME" : "doubleZero",
+        "%RANGE" : "uima.cas.Double"
+      },
+      "doubleOne" : {
+        "%NAME" : "doubleOne",
+        "%RANGE" : "uima.cas.Double"
+      },
+      "doublePosInfinity" : {
+        "%NAME" : "doublePosInfinity",
+        "%RANGE" : "uima.cas.Double"
+      },
+      "doubleNegInfinity" : {
+        "%NAME" : "doubleNegInfinity",
+        "%RANGE" : "uima.cas.Double"
+      },
+      "doubleNan" : {
+        "%NAME" : "doubleNan",
+        "%RANGE" : "uima.cas.Double"
+      },
+      "floatZero" : {
+        "%NAME" : "floatZero",
+        "%RANGE" : "uima.cas.Float"
+      },
+      "floatOne" : {
+        "%NAME" : "floatOne",
+        "%RANGE" : "uima.cas.Float"
+      },
+      "floatPosInfinity" : {
+        "%NAME" : "floatPosInfinity",
+        "%RANGE" : "uima.cas.Float"
+      },
+      "floatNegInfinity" : {
+        "%NAME" : "floatNegInfinity",
+        "%RANGE" : "uima.cas.Float"
+      },
+      "floatNan" : {
+        "%NAME" : "floatNan",
+        "%RANGE" : "uima.cas.Float"
+      }
+    }
+  },
+  "%FEATURE_STRUCTURES" : {
+    "1" : {
+      "%TYPE" : "SpecialValuesType",
+      "doubleZero" : 0.0,
+      "doubleOne" : 1.0,
+      "#doublePosInfinity" : "Infinity",
+      "#doubleNegInfinity" : "-Infinity",
+      "#doubleNan" : "NaN",
+      "floatZero" : 0.0,
+      "floatOne" : 1.0,
+      "#floatPosInfinity" : "Infinity",
+      "#floatNegInfinity" : "-Infinity",
+      "#floatNan" : "NaN"
+    },
+    "2" : {
+      "%TYPE" : "uima.cas.DoubleArray",
+      "%ELEMENTS" : [ 0.0, 1.0, "-Infinity", "Infinity", "NaN" ]
+    },
+    "3" : {
+      "%TYPE" : "uima.cas.FloatArray",
+      "%ELEMENTS" : [ 0.0, 1.0, "-Infinity", "Infinity", "NaN" ]
+    }
+  },
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 4,
+      "%MEMBERS" : [ 1, 2, 3 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithFloatingPointSpecialValues/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithFloatingPointSpecialValues/debug-typesystem.xml
new file mode 100644
index 0000000..9a8766d
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithFloatingPointSpecialValues/debug-typesystem.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+        <typeDescription>
+            <name>SpecialValuesType</name>
+            <description/>
+            <supertypeName>uima.cas.TOP</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>doubleZero</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doubleOne</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doublePosInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doubleNegInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doubleNan</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatZero</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatOne</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatPosInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatNegInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatNan</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithFloatingPointSpecialValues/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithFloatingPointSpecialValues/debug.xmi
new file mode 100644
index 0000000..e02d4cb
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithFloatingPointSpecialValues/debug.xmi
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:noNamespace="http:///uima/noNamespace.ecore" xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <noNamespace:SpecialValuesType xmi:id="1" doubleZero="0.0" doubleOne="1.0" doublePosInfinity="Infinity" doubleNegInfinity="-Infinity" doubleNan="NaN" floatZero="0.0" floatOne="1.0" floatPosInfinity="Infinity" floatNegInfinity="-Infinity" floatNan="NaN"/>
+    <cas:DoubleArray xmi:id="2" elements="0.0 1.0 -Infinity Infinity NaN"/>
+    <cas:FloatArray xmi:id="3" elements="0.0 1.0 -Infinity Infinity NaN"/>
+    <cas:View members="1 2 3"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithFloatingPointSpecialValues/typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithFloatingPointSpecialValues/typesystem.xml
new file mode 100644
index 0000000..9a8766d
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithFloatingPointSpecialValues/typesystem.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+        <typeDescription>
+            <name>SpecialValuesType</name>
+            <description/>
+            <supertypeName>uima.cas.TOP</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>doubleZero</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doubleOne</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doublePosInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doubleNegInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>doubleNan</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Double</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatZero</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatOne</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatPosInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatNegInfinity</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+                <featureDescription>
+                    <name>floatNan</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.Float</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithLeftToRightTextAndAnnotations/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithLeftToRightTextAndAnnotations/data.json
new file mode 100644
index 0000000..dfbbd65
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithLeftToRightTextAndAnnotations/data.json
@@ -0,0 +1,37 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : {
+    "1" : {
+      "%TYPE" : "uima.cas.Sofa",
+      "sofaNum" : 1,
+      "sofaID" : "_InitialView",
+      "mimeType" : "text",
+      "sofaString" : "هذا اختبار"
+    },
+    "2" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 0,
+      "end" : 3
+    },
+    "3" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 4,
+      "end" : 10
+    },
+    "4" : {
+      "%TYPE" : "uima.tcas.DocumentAnnotation",
+      "@sofa" : 1,
+      "begin" : 0,
+      "end" : 10,
+      "language" : "x-unspecified"
+    }
+  },
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ 2, 3, 4 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithLeftToRightTextAndAnnotations/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithLeftToRightTextAndAnnotations/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithLeftToRightTextAndAnnotations/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithLeftToRightTextAndAnnotations/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithLeftToRightTextAndAnnotations/debug.xmi
new file mode 100644
index 0000000..108d362
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithLeftToRightTextAndAnnotations/debug.xmi
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <tcas:Annotation xmi:id="2" sofa="1" begin="0" end="3"/>
+    <tcas:Annotation xmi:id="3" sofa="1" begin="4" end="10"/>
+    <tcas:DocumentAnnotation xmi:id="4" sofa="1" begin="0" end="10" language="x-unspecified"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView" mimeType="text" sofaString="هذا اختبار"/>
+    <cas:View sofa="1" members="2 3 4"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataArray/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataArray/data.json
new file mode 100644
index 0000000..b2e39d2
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataArray/data.json
@@ -0,0 +1,22 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : {
+    "1" : {
+      "%TYPE" : "uima.cas.ByteArray",
+      "%ELEMENTS" : "VGhpcyBpcyBhIHRlc3Q="
+    },
+    "2" : {
+      "%TYPE" : "uima.cas.Sofa",
+      "sofaNum" : 1,
+      "sofaID" : "_InitialView",
+      "mimeType" : "text/plain",
+      "@sofaArray" : 1
+    }
+  },
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 2,
+      "%MEMBERS" : [ ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataArray/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataArray/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataArray/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataArray/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataArray/debug.xmi
new file mode 100644
index 0000000..89075f6
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataArray/debug.xmi
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <cas:Sofa xmi:id="2" sofaNum="1" sofaID="_InitialView" mimeType="text/plain" sofaArray="1"/>
+    <cas:ByteArray xmi:id="1" elements="5468697320697320612074657374"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataURI/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataURI/data.json
new file mode 100644
index 0000000..a02bb76
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataURI/data.json
@@ -0,0 +1,18 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : {
+    "1" : {
+      "%TYPE" : "uima.cas.Sofa",
+      "sofaNum" : 1,
+      "sofaID" : "_InitialView",
+      "mimeType" : "text/plain",
+      "sofaURI" : "classpath:/ProgrammaticallyCreatedCasDataSuite/document.txt"
+    }
+  },
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataURI/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataURI/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataURI/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataURI/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataURI/debug.xmi
new file mode 100644
index 0000000..89966e0
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithSofaDataURI/debug.xmi
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView" mimeType="text/plain" sofaURI="classpath:/ProgrammaticallyCreatedCasDataSuite/document.txt"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithText/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithText/data.json
new file mode 100644
index 0000000..4d32065
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithText/data.json
@@ -0,0 +1,25 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : {
+    "1" : {
+      "%TYPE" : "uima.cas.Sofa",
+      "sofaNum" : 1,
+      "sofaID" : "_InitialView",
+      "mimeType" : "text",
+      "sofaString" : "This is a test."
+    },
+    "2" : {
+      "%TYPE" : "uima.tcas.DocumentAnnotation",
+      "@sofa" : 1,
+      "begin" : 0,
+      "end" : 15,
+      "language" : "x-unspecified"
+    }
+  },
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ 2 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithText/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithText/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithText/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithText/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithText/debug.xmi
new file mode 100644
index 0000000..943df5f
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithText/debug.xmi
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <tcas:DocumentAnnotation xmi:id="2" sofa="1" begin="0" end="15" language="x-unspecified"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView" mimeType="text" sofaString="This is a test."/>
+    <cas:View sofa="1" members="2"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTextAndAnnotations/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTextAndAnnotations/data.json
new file mode 100644
index 0000000..6df3290
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTextAndAnnotations/data.json
@@ -0,0 +1,49 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : {
+    "1" : {
+      "%TYPE" : "uima.cas.Sofa",
+      "sofaNum" : 1,
+      "sofaID" : "_InitialView",
+      "mimeType" : "text",
+      "sofaString" : "This is a test"
+    },
+    "2" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 0,
+      "end" : 4
+    },
+    "3" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 5,
+      "end" : 7
+    },
+    "4" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 8,
+      "end" : 9
+    },
+    "5" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 10,
+      "end" : 14
+    },
+    "6" : {
+      "%TYPE" : "uima.tcas.DocumentAnnotation",
+      "@sofa" : 1,
+      "begin" : 0,
+      "end" : 14,
+      "language" : "x-unspecified"
+    }
+  },
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ 2, 3, 4, 5, 6 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTextAndAnnotations/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTextAndAnnotations/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTextAndAnnotations/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTextAndAnnotations/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTextAndAnnotations/debug.xmi
new file mode 100644
index 0000000..37c1e9b
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTextAndAnnotations/debug.xmi
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <tcas:Annotation xmi:id="2" sofa="1" begin="0" end="4"/>
+    <tcas:Annotation xmi:id="3" sofa="1" begin="5" end="7"/>
+    <tcas:Annotation xmi:id="4" sofa="1" begin="8" end="9"/>
+    <tcas:Annotation xmi:id="5" sofa="1" begin="10" end="14"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView"/>
+    <cas:View sofa="1" members="2 3 4 5"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/data.json
new file mode 100644
index 0000000..a90d82c
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/data.json
@@ -0,0 +1,49 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : {
+    "1" : {
+      "%TYPE" : "uima.cas.Sofa",
+      "sofaNum" : 1,
+      "sofaID" : "_InitialView",
+      "mimeType" : "text",
+      "sofaString" : "這是一個測試"
+    },
+    "2" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 0,
+      "end" : 1
+    },
+    "3" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 1,
+      "end" : 2
+    },
+    "4" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 2,
+      "end" : 4
+    },
+    "5" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 4,
+      "end" : 6
+    },
+    "6" : {
+      "%TYPE" : "uima.tcas.DocumentAnnotation",
+      "@sofa" : 1,
+      "begin" : 0,
+      "end" : 6,
+      "language" : "x-unspecified"
+    }
+  },
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ 2, 3, 4, 5, 6 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/debug.xmi
new file mode 100644
index 0000000..0087d72
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithTraditionalChineseTextAndAnnotations/debug.xmi
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <tcas:Annotation xmi:id="2" sofa="1" begin="0" end="1"/>
+    <tcas:Annotation xmi:id="3" sofa="1" begin="1" end="2"/>
+    <tcas:Annotation xmi:id="4" sofa="1" begin="2" end="4"/>
+    <tcas:Annotation xmi:id="5" sofa="1" begin="4" end="6"/>
+    <tcas:DocumentAnnotation xmi:id="6" sofa="1" begin="0" end="6" language="x-unspecified"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView" mimeType="text" sofaString="這是一個測試"/>
+    <cas:View sofa="1" members="2 3 4 5 6"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithoutTextButWithAnnotations/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithoutTextButWithAnnotations/data.json
new file mode 100644
index 0000000..25a71c9
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithoutTextButWithAnnotations/data.json
@@ -0,0 +1,40 @@
+{
+  "%TYPES" : { },
+  "%FEATURE_STRUCTURES" : {
+    "1" : {
+      "%TYPE" : "uima.cas.Sofa",
+      "sofaNum" : 1,
+      "sofaID" : "_InitialView"
+    },
+    "2" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 0,
+      "end" : 4
+    },
+    "3" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 5,
+      "end" : 7
+    },
+    "4" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 8,
+      "end" : 9
+    },
+    "5" : {
+      "%TYPE" : "uima.tcas.Annotation",
+      "@sofa" : 1,
+      "begin" : 10,
+      "end" : 14
+    }
+  },
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ 2, 3, 4, 5 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithoutTextButWithAnnotations/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithoutTextButWithAnnotations/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithoutTextButWithAnnotations/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithoutTextButWithAnnotations/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithoutTextButWithAnnotations/debug.xmi
new file mode 100644
index 0000000..37c1e9b
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/casWithoutTextButWithAnnotations/debug.xmi
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+    <tcas:Annotation xmi:id="2" sofa="1" begin="0" end="4"/>
+    <tcas:Annotation xmi:id="3" sofa="1" begin="5" end="7"/>
+    <tcas:Annotation xmi:id="4" sofa="1" begin="8" end="9"/>
+    <tcas:Annotation xmi:id="5" sofa="1" begin="10" end="14"/>
+    <cas:Sofa xmi:id="1" sofaNum="1" sofaID="_InitialView"/>
+    <cas:View sofa="1" members="2 3 4 5"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/emptyCas/data.json b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/emptyCas/data.json
new file mode 100644
index 0000000..fcd8582
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/emptyCas/data.json
@@ -0,0 +1,9 @@
+{
+  "%TYPES" : { },
+  "%VIEWS" : {
+    "_InitialView" : {
+      "%SOFA" : 1,
+      "%MEMBERS" : [ ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/emptyCas/debug-typesystem.xml b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/emptyCas/debug-typesystem.xml
new file mode 100644
index 0000000..07e327a
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/emptyCas/debug-typesystem.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<typeSystemDescription xmlns="http://uima.apache.org/resourceSpecifier">
+    <types>
+        <typeDescription>
+            <name>uima.tcas.DocumentAnnotation</name>
+            <description/>
+            <supertypeName>uima.tcas.Annotation</supertypeName>
+            <features>
+                <featureDescription>
+                    <name>language</name>
+                    <description/>
+                    <rangeTypeName>uima.cas.String</rangeTypeName>
+                </featureDescription>
+            </features>
+        </typeDescription>
+    </types>
+</typeSystemDescription>
diff --git a/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/emptyCas/debug.xmi b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/emptyCas/debug.xmi
new file mode 100644
index 0000000..6fd88bd
--- /dev/null
+++ b/uimaj-json/src/test/resources/CasSerializationDeserialization_JsonCas2_FsAsObject_Test/ser-ref/emptyCas/debug.xmi
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?><xmi:XMI xmlns:tcas="http:///uima/tcas.ecore" xmlns:xmi="http://www.omg.org/XMI" xmlns:cas="http:///uima/cas.ecore" xmi:version="2.0">
+    <cas:NULL xmi:id="0"/>
+</xmi:XMI>
diff --git a/uimaj-json/src/test/resources/FlexJsonDeserializer/feature_structures_only.json b/uimaj-json/src/test/resources/FlexJsonDeserializer/feature_structures_only.json
new file mode 100644
index 0000000..66769ed
--- /dev/null
+++ b/uimaj-json/src/test/resources/FlexJsonDeserializer/feature_structures_only.json
@@ -0,0 +1,23 @@
+[ {
+  "%ID" : "0",
+  "%TYPE" : "Sofa",
+  "sofaNum" : 1,
+  "sofaID" : "_InitialView",
+  "mimeType" : "text",
+  "sofaString" : "Hello world"
+}, {
+  "%ID" : "1",
+  "%TYPE" : "Annotation",
+  "%VIEWS" : [ "_InitialView" ],
+  "@sofa" : "0",
+  "begin" : 0,
+  "end" : 10
+}, {
+  "%ID" : "2",
+  "%TYPE" : "DocumentAnnotation",
+  "%VIEWS" : [ "_InitialView" ],
+  "@sofa" : "0",
+  "begin" : 0,
+  "end" : 11,
+  "language" : "en"
+} ]
diff --git a/uimaj-json/src/test/resources/FlexJsonDeserializer/text_only.json b/uimaj-json/src/test/resources/FlexJsonDeserializer/text_only.json
new file mode 100644
index 0000000..6bdab32
--- /dev/null
+++ b/uimaj-json/src/test/resources/FlexJsonDeserializer/text_only.json
@@ -0,0 +1 @@
+"Hello world."
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/FlexJsonSerializerTest/customAnnotationType/reference.json b/uimaj-json/src/test/resources/FlexJsonSerializerTest/customAnnotationType/reference.json
new file mode 100644
index 0000000..dd44fc2
--- /dev/null
+++ b/uimaj-json/src/test/resources/FlexJsonSerializerTest/customAnnotationType/reference.json
@@ -0,0 +1,21 @@
+{
+  "%TYPES" : {
+    "custom.Annotation" : {
+      "%SUPER_TYPE" : "uima.tcas.Annotation",
+      "value" : "uima.cas.String"
+    }
+  },
+  "%FEATURE_STRUCTURES" : [ {
+    "%ID" : "0",
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 1,
+    "sofaID" : "_InitialView"
+  }, {
+    "%ID" : "1",
+    "%TYPE" : "custom.Annotation",
+    "%VIEWS" : [ "_InitialView" ],
+    "@sofa" : "0",
+    "begin" : 0,
+    "end" : 10
+  } ]
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/FlexJsonSerializerTest/featureStructureIndexedInMultipleViewsInline/reference.json b/uimaj-json/src/test/resources/FlexJsonSerializerTest/featureStructureIndexedInMultipleViewsInline/reference.json
new file mode 100644
index 0000000..571f94b
--- /dev/null
+++ b/uimaj-json/src/test/resources/FlexJsonSerializerTest/featureStructureIndexedInMultipleViewsInline/reference.json
@@ -0,0 +1,39 @@
+{
+  "%FEATURE_STRUCTURES" : [ {
+    "%ID" : "0",
+    "%TYPE" : "uima.cas.TOP",
+    "%VIEWS" : [ "_InitialView", "secondView" ]
+  }, {
+    "%ID" : "1",
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 1,
+    "sofaID" : "_InitialView",
+    "mimeType" : "text",
+    "sofaString" : "First view"
+  }, {
+    "%ID" : "2",
+    "%TYPE" : "uima.tcas.DocumentAnnotation",
+    "%VIEWS" : [ "_InitialView" ],
+    "%FLAGS" : [ "DocumentAnnotation" ],
+    "@sofa" : "1",
+    "begin" : 0,
+    "end" : 10,
+    "language" : "x-unspecified"
+  }, {
+    "%ID" : "3",
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 2,
+    "sofaID" : "secondView",
+    "mimeType" : "text",
+    "sofaString" : "Second view"
+  }, {
+    "%ID" : "4",
+    "%TYPE" : "uima.tcas.DocumentAnnotation",
+    "%VIEWS" : [ "secondView" ],
+    "%FLAGS" : [ "DocumentAnnotation" ],
+    "@sofa" : "3",
+    "begin" : 0,
+    "end" : 11,
+    "language" : "x-unspecified"
+  } ]
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/FlexJsonSerializerTest/featureStructureIndexedInMultipleViewsSeparate/reference.json b/uimaj-json/src/test/resources/FlexJsonSerializerTest/featureStructureIndexedInMultipleViewsSeparate/reference.json
new file mode 100644
index 0000000..e3d7825
--- /dev/null
+++ b/uimaj-json/src/test/resources/FlexJsonSerializerTest/featureStructureIndexedInMultipleViewsSeparate/reference.json
@@ -0,0 +1,40 @@
+{
+  "%VIEWS" : {
+    "_InitialView" : [ "0", "1", "2" ],
+    "secondView" : [ "3", "4", "2" ]
+  },
+  "%FEATURE_STRUCTURES" : [ {
+    "%ID" : "2",
+    "%TYPE" : "uima.cas.TOP"
+  }, {
+    "%ID" : "0",
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 1,
+    "sofaID" : "_InitialView",
+    "mimeType" : "text",
+    "sofaString" : "First view"
+  }, {
+    "%ID" : "1",
+    "%TYPE" : "uima.tcas.DocumentAnnotation",
+    "%FLAGS" : [ "DocumentAnnotation" ],
+    "@sofa" : "0",
+    "begin" : 0,
+    "end" : 10,
+    "language" : "x-unspecified"
+  }, {
+    "%ID" : "3",
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 2,
+    "sofaID" : "secondView",
+    "mimeType" : "text",
+    "sofaString" : "Second view"
+  }, {
+    "%ID" : "4",
+    "%TYPE" : "uima.tcas.DocumentAnnotation",
+    "%FLAGS" : [ "DocumentAnnotation" ],
+    "@sofa" : "3",
+    "begin" : 0,
+    "end" : 11,
+    "language" : "x-unspecified"
+  } ]
+}
\ No newline at end of file
diff --git a/uimaj-json/src/test/resources/FlexJsonSerializerTest/multipleViewsAndSofas/reference.json b/uimaj-json/src/test/resources/FlexJsonSerializerTest/multipleViewsAndSofas/reference.json
new file mode 100644
index 0000000..33c5fea
--- /dev/null
+++ b/uimaj-json/src/test/resources/FlexJsonSerializerTest/multipleViewsAndSofas/reference.json
@@ -0,0 +1,35 @@
+{
+  "%FEATURE_STRUCTURES" : [ {
+    "%ID" : "0",
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 1,
+    "sofaID" : "_InitialView",
+    "mimeType" : "text",
+    "sofaString" : "First view"
+  }, {
+    "%ID" : "1",
+    "%TYPE" : "uima.tcas.DocumentAnnotation",
+    "%VIEWS" : [ "_InitialView" ],
+    "%FLAGS" : [ "DocumentAnnotation" ],
+    "@sofa" : "0",
+    "begin" : 0,
+    "end" : 10,
+    "language" : "x-unspecified"
+  }, {
+    "%ID" : "2",
+    "%TYPE" : "uima.cas.Sofa",
+    "sofaNum" : 2,
+    "sofaID" : "secondView",
+    "mimeType" : "text",
+    "sofaString" : "Second view"
+  }, {
+    "%ID" : "3",
+    "%TYPE" : "uima.tcas.DocumentAnnotation",
+    "%VIEWS" : [ "secondView" ],
+    "%FLAGS" : [ "DocumentAnnotation" ],
+    "@sofa" : "2",
+    "begin" : 0,
+    "end" : 11,
+    "language" : "x-unspecified"
+  } ]
+}
\ No newline at end of file
diff --git a/uimaj-parent/pom.xml b/uimaj-parent/pom.xml
index 8701bbe..8f6a36e 100644
--- a/uimaj-parent/pom.xml
+++ b/uimaj-parent/pom.xml
@@ -153,7 +153,7 @@
     --> 
     <slf4j.version>1.7.25</slf4j.version>
     <log4j.version>2.10.0</log4j.version>
-    <jackson.version>2.9.2</jackson.version>
+    <jackson.version>2.12.4</jackson.version>
     <junit.version>5.7.2</junit.version>
     
     <maven.compiler.target>1.8</maven.compiler.target>