You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by js...@apache.org on 2019/11/12 16:18:21 UTC

[sis] 01/01: GeoJson : add GeoJson DataStore

This is an automated email from the ASF dual-hosted git repository.

jsorel pushed a commit to branch feat/geojson
in repository https://gitbox.apache.org/repos/asf/sis.git

commit fb85b0adda25b99acc0f58f0334425a45a240718
Author: jsorel <jo...@geomatys.com>
AuthorDate: Tue Oct 29 17:07:57 2019 +0100

    GeoJson : add GeoJson DataStore
---
 storage/pom.xml                                    |   1 +
 storage/{ => sis-geojson}/pom.xml                  | 102 +--
 .../org/apache/sis/storage/geojson/Bundle.java     | 218 ++++++
 .../apache/sis/storage/geojson/Bundle.properties   |   9 +
 .../sis/storage/geojson/Bundle_en.properties       |   9 +
 .../sis/storage/geojson/Bundle_fr.properties       |   9 +
 .../sis/storage/geojson/GeoJSONFileWriter.java     | 133 ++++
 .../sis/storage/geojson/GeoJSONProvider.java       | 181 +++++
 .../apache/sis/storage/geojson/GeoJSONReader.java  | 347 +++++++++
 .../apache/sis/storage/geojson/GeoJSONStore.java   | 452 ++++++++++++
 .../sis/storage/geojson/GeoJSONStreamWriter.java   | 221 ++++++
 .../apache/sis/storage/geojson/GeoJSONWriter.java  | 455 ++++++++++++
 .../sis/storage/geojson/binding/GeoJSONCRS.java    |  94 +++
 .../storage/geojson/binding/GeoJSONFeature.java    |  68 ++
 .../geojson/binding/GeoJSONFeatureCollection.java  | 189 +++++
 .../storage/geojson/binding/GeoJSONGeometry.java   | 177 +++++
 .../sis/storage/geojson/binding/GeoJSONObject.java |  77 ++
 .../storage/geojson/utils/FeatureTypeUtils.java    | 596 +++++++++++++++
 .../geojson/utils/GeoJSONFeatureIterator.java      |  44 ++
 .../sis/storage/geojson/utils/GeoJSONMembres.java  |  42 ++
 .../sis/storage/geojson/utils/GeoJSONParser.java   | 680 +++++++++++++++++
 .../sis/storage/geojson/utils/GeoJSONTypes.java    |  48 ++
 .../sis/storage/geojson/utils/GeoJSONUtils.java    | 581 +++++++++++++++
 .../sis/storage/geojson/utils/GeometryUtils.java   | 353 +++++++++
 .../storage/geojson/utils/LiteJsonLocation.java    | 104 +++
 .../org.apache.sis.storage.DataStoreProvider       |   1 +
 .../storage/geojson/FeatureTypeUtilsTest.java      | 265 +++++++
 .../internal/storage/geojson/GeoJSONReadTest.java  | 351 +++++++++
 .../internal/storage/geojson/GeoJSONWriteTest.java | 577 +++++++++++++++
 .../storage/geojson/LiteJsonLocationTest.java      | 112 +++
 .../apache/sis/test/suite/GeoJSONTestSuite.java    |  34 +
 .../sis/internal/storage/geojson/f_prop_array.json |   7 +
 .../sis/internal/storage/geojson/feature.json      |  69 ++
 .../storage/geojson/featurecollection.json         | 158 ++++
 .../internal/storage/geojson/geometries.properties |   8 +
 .../storage/geojson/geometrycollection.json        | 188 +++++
 .../sis/internal/storage/geojson/linestring.json   | 109 +++
 .../sis/internal/storage/geojson/longValue.json    |  24 +
 .../internal/storage/geojson/multilinestring.json  |  85 +++
 .../sis/internal/storage/geojson/multipoint.json   |  13 +
 .../sis/internal/storage/geojson/multipolygon.json | 815 +++++++++++++++++++++
 .../apache/sis/internal/storage/geojson/point.json |   7 +
 .../sis/internal/storage/geojson/polygon.json      | 789 ++++++++++++++++++++
 .../geojson/sample_with_null_properties.json       | 502 +++++++++++++
 44 files changed, 9229 insertions(+), 75 deletions(-)

diff --git a/storage/pom.xml b/storage/pom.xml
index fe99bdb..081c953 100644
--- a/storage/pom.xml
+++ b/storage/pom.xml
@@ -169,6 +169,7 @@
     <module>sis-sqlstore</module>
     <module>sis-netcdf</module>
     <module>sis-geotiff</module>
+    <module>sis-geojson</module>
     <module>sis-earth-observation</module>
     <module>sis-gdal</module>
   </modules>
diff --git a/storage/pom.xml b/storage/sis-geojson/pom.xml
similarity index 54%
copy from storage/pom.xml
copy to storage/sis-geojson/pom.xml
index fe99bdb..3c49d4d 100644
--- a/storage/pom.xml
+++ b/storage/sis-geojson/pom.xml
@@ -27,7 +27,7 @@
 
   <parent>
     <groupId>org.apache.sis</groupId>
-    <artifactId>parent</artifactId>
+    <artifactId>storage</artifactId>
     <version>2.0-SNAPSHOT</version>
   </parent>
 
@@ -35,12 +35,11 @@
   <!-- ===========================================================
            Module Description
        =========================================================== -->
-  <artifactId>storage</artifactId>
-  <packaging>pom</packaging>
-  <name>Apache SIS storage</name>
+  <groupId>org.apache.sis.storage</groupId>
+  <artifactId>sis-geojson</artifactId>
+  <name>Apache SIS GeoJSON storage</name>
   <description>
-    Group of modules for reading and writing data from/to various storages.
-    Storages are typically file formats or a database schemas.
+    Geojson DataStore implementation.
   </description>
 
 
@@ -72,26 +71,6 @@
     </developer>
   </developers>
   <contributors>
-    <contributor>
-      <name>Thi Phuong Hao Nguyen</name>
-      <email>nguyenthiphuonghao243@gmail.com</email>
-      <organization>VNSC</organization>
-      <organizationUrl>http://vnsc.org.vn</organizationUrl>
-      <timezone>+7</timezone>
-      <roles>
-        <role>developer</role>
-      </roles>
-    </contributor>
-    <contributor>
-      <name>Minh Chinh Vu</name>
-      <email>chinhvm.uet.1995@gmail.com</email>
-      <organization>VNSC</organization>
-      <organizationUrl>http://vnsc.org.vn</organizationUrl>
-      <timezone>+7</timezone>
-      <roles>
-        <role>developer</role>
-      </roles>
-    </contributor>
   </contributors>
 
 
@@ -100,21 +79,18 @@
        =========================================================== -->
   <build>
     <plugins>
-
-      <!-- Compile properties files into resources UTF files and
-           collect JAR files in <root>/target/binaries directory. -->
       <plugin>
-        <groupId>org.apache.sis.core</groupId>
-        <artifactId>sis-build-helper</artifactId>
-        <version>${sis.plugin.version}</version>
-        <executions>
-          <execution>
-            <goals>
-              <goal>compile-resources</goal>
-              <goal>collect-jars</goal>
-            </goals>
-          </execution>
-        </executions>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <archive>
+            <manifestEntries>
+              <Automatic-Module-Name>
+                org.apache.sis.storage.geojson
+              </Automatic-Module-Name>
+            </manifestEntries>
+          </archive>
+        </configuration>
       </plugin>
     </plugins>
   </build>
@@ -125,52 +101,28 @@
        =========================================================== -->
   <dependencies>
     <dependency>
-      <groupId>org.apache.sis.core</groupId>
-      <artifactId>sis-metadata</artifactId>
+      <groupId>org.apache.sis.storage</groupId>
+      <artifactId>sis-storage</artifactId>
       <version>${project.version}</version>
     </dependency>
     <dependency>
       <groupId>org.apache.sis.core</groupId>
-      <artifactId>sis-referencing</artifactId>
+      <artifactId>sis-cql</artifactId>
       <version>${project.version}</version>
     </dependency>
     <dependency>
-      <groupId>org.opengis</groupId>
-      <artifactId>geoapi-pending</artifactId>
+      <groupId>org.locationtech.jts</groupId>
+      <artifactId>jts-core</artifactId>
     </dependency>
-
-    <!-- Test dependencies -->
     <dependency>
-      <groupId>org.opengis</groupId>
-      <artifactId>geoapi-conformance</artifactId>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>2.10.0</version>
     </dependency>
-    <dependency>
-      <groupId>org.apache.derby</groupId>
-      <artifactId>derby</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.sis.core</groupId>
-      <artifactId>sis-utility</artifactId>
-      <version>${project.version}</version>
-      <type>test-jar</type>
-      <scope>test</scope>
-    </dependency>
-  </dependencies>
 
+  </dependencies>
 
-  <!-- ===========================================================
-           Sub-modules included in the build
-       =========================================================== -->
-  <modules>
-    <module>sis-storage</module>
-    <module>sis-shapefile</module>
-    <module>sis-xmlstore</module>
-    <module>sis-sqlstore</module>
-    <module>sis-netcdf</module>
-    <module>sis-geotiff</module>
-    <module>sis-earth-observation</module>
-    <module>sis-gdal</module>
-  </modules>
+  <repositories>
+  </repositories>
 
 </project>
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.java
new file mode 100644
index 0000000..b6074e0
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.java
@@ -0,0 +1,218 @@
+/*
+ * 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.sis.storage.geojson;
+
+import java.util.Locale;
+import java.util.ResourceBundle;
+import java.util.MissingResourceException;
+import org.opengis.util.InternationalString;
+import org.apache.sis.util.iso.ResourceInternationalString;
+import org.apache.sis.util.resources.IndexedResourceBundle;
+
+
+/**
+ * Locale-dependent resources for words or simple sentences.
+ */
+public final class Bundle extends IndexedResourceBundle {
+    /**
+     * Resource keys. This class is used when compiling sources, but no dependencies to
+     * {@code Keys} should appear in any resulting class files. Since the Java compiler
+     * inlines final integer values, using long identifiers will not bloat the constant
+     * pools of compiled classes.
+     */
+    public static final class Keys {
+        private Keys() {
+        }
+
+        /**
+         * Number of decimals
+         */
+        public static final short coordinate_accuracy = 1;
+
+        /**
+         * Number of decimals (default 7).
+         */
+        public static final short coordinate_accuracy_remarks = 2;
+
+        /**
+         * GeoJSON data file (.json)
+         */
+        public static final short datastoreDescription = 3;
+
+        /**
+         * multiple GeoJSON data files (.json)
+         */
+        public static final short datastoreFolderDescription = 4;
+
+        /**
+         * GeoJSON (folder)
+         */
+        public static final short datastoreFolderTitle = 5;
+
+        /**
+         * GeoJSON
+         */
+        public static final short datastoreTitle = 6;
+    }
+
+    /**
+     * Constructs a new resource bundle loading data from the given UTF file.
+     *
+     * @param filename The file or the JAR entry containing resources.
+     */
+    public Bundle(final java.net.URL filename) {
+        super(filename);
+    }
+
+    /**
+     * Returns resources in the given locale.
+     *
+     * @param  locale The locale, or {@code null} for the default locale.
+     * @return Resources in the given locale.
+     * @throws MissingResourceException if resources can't be found.
+     */
+    public static Bundle getResources(Locale locale) throws MissingResourceException {
+        return getBundle(Bundle.class, locale);
+    }
+
+    /**
+     * The international string to be returned by {@link formatInternational}.
+     */
+    private static final class International extends ResourceInternationalString {
+        private static final long serialVersionUID = -9199238559657784488L;
+
+        International(final int key) {
+            super(Bundle.class.getName(), String.valueOf(key));
+        }
+
+        @Override
+        protected ResourceBundle getBundle(final Locale locale) {
+            return getResources(locale);
+        }
+    }
+
+    /**
+     * Gets an international string for the given key. This method does not check for the key
+     * validity. If the key is invalid, then a {@link MissingResourceException} may be thrown
+     * when a {@link InternationalString#toString} method is invoked.
+     *
+     * @param  key The key for the desired string.
+     * @return An international string for the given key.
+     */
+    public static InternationalString formatInternational(final short key) {
+        return new International(key);
+    }
+
+    /**
+     * Gets an international string for the given key. This method does not check for the key
+     * validity. If the key is invalid, then a {@link MissingResourceException} may be thrown
+     * when a {@link InternationalString#toString} method is invoked.
+     *
+     * {@note This method is redundant with the one expecting <code>Object...</code>, but is
+     *        provided for binary compatibility with previous Geotk versions. It also avoid the
+     *        creation of a temporary array. There is no risk of confusion since the two methods
+     *        delegate their work to the same <code>format</code> method anyway.}
+     *
+     * @param  key The key for the desired string.
+     * @param  arg Values to substitute to "{0}".
+     * @return An international string for the given key.
+     *
+     * @todo Current implementation just invokes {@link #format}. Need to format only when
+     *       {@code toString(Locale)} is invoked.
+     */
+    public static InternationalString formatInternational(final short key, final Object arg) {
+        return new org.apache.sis.util.iso.SimpleInternationalString(format(key, arg));
+    }
+
+    /**
+     * Gets an international string for the given key. This method does not check for the key
+     * validity. If the key is invalid, then a {@link MissingResourceException} may be thrown
+     * when a {@link InternationalString#toString} method is invoked.
+     *
+     * @param  key The key for the desired string.
+     * @param  args Values to substitute to "{0}", "{1}", <i>etc</i>.
+     * @return An international string for the given key.
+     *
+     * @todo Current implementation just invokes {@link #format}. Need to format only when
+     *       {@code toString(Locale)} is invoked.
+     */
+    public static InternationalString formatInternational(final short key, final Object... args) {
+        return new org.apache.sis.util.iso.SimpleInternationalString(format(key, args));
+    }
+
+    /**
+     * Gets a string for the given key from this resource bundle or one of its parents.
+     *
+     * @param  key The key for the desired string.
+     * @return The string for the given key.
+     * @throws MissingResourceException If no object for the given key can be found.
+     */
+    public static String format(final short key) throws MissingResourceException {
+        return getResources(null).getString(key);
+    }
+
+    /**
+     * Gets a string for the given key are replace all occurrence of "{0}"
+     * with values of {@code arg0}.
+     *
+     * @param  key The key for the desired string.
+     * @param  arg0 Value to substitute to "{0}".
+     * @return The formatted string for the given key.
+     * @throws MissingResourceException If no object for the given key can be found.
+     */
+    public static String format(final short  key,
+                                final Object arg0) throws MissingResourceException
+    {
+        return getResources(null).getString(key, arg0);
+    }
+
+    /**
+     * Gets a string for the given key are replace all occurrence of "{0}",
+     * "{1}", with values of {@code arg0}, {@code arg1}.
+     *
+     * @param  key The key for the desired string.
+     * @param  arg0 Value to substitute to "{0}".
+     * @param  arg1 Value to substitute to "{1}".
+     * @return The formatted string for the given key.
+     * @throws MissingResourceException If no object for the given key can be found.
+     */
+    public static String format(final short  key,
+                                final Object arg0,
+                                final Object arg1) throws MissingResourceException
+    {
+        return getResources(null).getString(key, arg0, arg1);
+    }
+
+    /**
+     * Gets a string for the given key are replace all occurrence of "{0}",
+     * "{1}", with values of {@code arg0}, {@code arg1}, etc.
+     *
+     * @param  key The key for the desired string.
+     * @param  arg0 Value to substitute to "{0}".
+     * @param  arg1 Value to substitute to "{1}".
+     * @param  arg2 Value to substitute to "{2}".
+     * @return The formatted string for the given key.
+     * @throws MissingResourceException If no object for the given key can be found.
+     */
+    public static String format(final short  key,
+                                final Object arg0,
+                                final Object arg1,
+                                final Object arg2) throws MissingResourceException
+    {
+        return getResources(null).getString(key, arg0, arg1, arg2);
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.properties b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.properties
new file mode 100644
index 0000000..9b25fe2
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.properties
@@ -0,0 +1,9 @@
+# To change this template, choose Tools | Templates
+# and open the template in the editor.
+
+datastoreTitle=GeoJSON
+datastoreDescription=GeoJSON data file (.json)
+datastoreFolderTitle=GeoJSON (folder)
+datastoreFolderDescription=multiple GeoJSON data files (.json)
+coordinate_accuracy=Number of decimals
+coordinate_accuracy_remarks=Number of decimals (default 7).
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_en.properties b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_en.properties
new file mode 100644
index 0000000..9b25fe2
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_en.properties
@@ -0,0 +1,9 @@
+# To change this template, choose Tools | Templates
+# and open the template in the editor.
+
+datastoreTitle=GeoJSON
+datastoreDescription=GeoJSON data file (.json)
+datastoreFolderTitle=GeoJSON (folder)
+datastoreFolderDescription=multiple GeoJSON data files (.json)
+coordinate_accuracy=Number of decimals
+coordinate_accuracy_remarks=Number of decimals (default 7).
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_fr.properties b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_fr.properties
new file mode 100644
index 0000000..b905f93
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_fr.properties
@@ -0,0 +1,9 @@
+# To change this template, choose Tools | Templates
+# and open the template in the editor.
+
+datastoreTitle=GeoJSON
+datastoreDescription=fichier GeoJSON de donn\u00e9es vectorielles (.json)
+datastoreFolderTitle=GeoJSON (dossier)
+datastoreFolderDescription=Multiples fichiers de donn\u00e9es GeoJSON (.json)
+coordinate_accuracy=Chiffres apr\u00e8s la virgule
+coordinate_accuracy_remarks=Nombre de chiffres apr\u00e8s la virgule.
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONFileWriter.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONFileWriter.java
new file mode 100644
index 0000000..c0d8e2b
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONFileWriter.java
@@ -0,0 +1,133 @@
+/*
+ * 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.sis.storage.geojson;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import java.io.Closeable;
+import org.apache.sis.storage.DataStoreException;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Iterator;
+import java.util.concurrent.locks.ReadWriteLock;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+class GeoJSONFileWriter extends GeoJSONReader {
+
+    private final GeoJSONWriter writer;
+
+    private Feature edited = null;
+    private Feature lastWritten = null;
+    private Path tmpFile;
+
+    public GeoJSONFileWriter(Path jsonFile, FeatureType featureType, ReadWriteLock rwLock,
+                             final String encoding, final int doubleAccuracy) throws DataStoreException {
+        super(jsonFile, featureType, rwLock);
+
+        JsonEncoding jsonEncoding = JsonEncoding.UTF8;
+
+        try {
+            final String name = featureType.getName().tip().toString();
+            tmpFile = jsonFile.resolveSibling(name + ".wjson");
+            writer = new GeoJSONWriter(tmpFile, jsonEncoding, doubleAccuracy, false);
+
+            //start write feature collection.
+            writer.writeStartFeatureCollection(crs, null);
+            writer.flush();
+        } catch (IOException ex) {
+            throw new DataStoreException(ex.getMessage(), ex);
+        }
+    }
+
+    @Override
+    public FeatureType getFeatureType() {
+        return super.getFeatureType();
+    }
+
+    @Override
+    public Feature next() throws BackingStoreException {
+        try {
+            write();
+            edited = super.next();
+        } catch (BackingStoreException ex) {
+            //we reach append mode
+            //create empty feature
+            edited = featureType.newInstance();
+            if (hasIdentifier) {
+                edited.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), idConverter.apply(currentFeatureIdx++));
+            }
+        }
+        return edited;
+    }
+
+    public void write(Feature edited) throws BackingStoreException {
+        this.edited = edited;
+        write();
+    }
+
+    public void write() throws BackingStoreException {
+        if (edited == null || edited.equals(lastWritten)) return;
+
+        lastWritten = edited;
+        try {
+            writer.writeFeature(edited);
+            writer.flush();
+        } catch (IOException | IllegalArgumentException e) {
+            throw new BackingStoreException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void remove() {
+        edited = null;
+    }
+
+    @Override
+    public void close() {
+        try (final GeoJSONWriter toClose = writer) {
+            toClose.writeEndFeatureCollection();
+            toClose.flush();
+        } catch (IOException ex) {
+            throw new BackingStoreException(ex);
+        } finally {
+            super.close();
+        }
+
+        //flip files
+        rwlock.writeLock().lock();
+        try {
+            Files.move(tmpFile, jsonFile, StandardCopyOption.REPLACE_EXISTING);
+        } catch (IOException ex) {
+            throw new BackingStoreException(ex);
+        } finally {
+            rwlock.writeLock().unlock();
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONProvider.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONProvider.java
new file mode 100644
index 0000000..ab7148a
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONProvider.java
@@ -0,0 +1,181 @@
+/*
+ * 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.sis.storage.geojson;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.sis.internal.storage.Capability;
+import org.apache.sis.internal.storage.StoreMetadata;
+import org.apache.sis.internal.storage.io.IOUtilities;
+import org.apache.sis.parameter.ParameterBuilder;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreProvider;
+import static org.apache.sis.storage.DataStoreProvider.LOCATION;
+import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.ProbeResult;
+import org.apache.sis.storage.StorageConnector;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.parameter.ParameterValueGroup;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+@StoreMetadata(
+        formatName = GeoJSONProvider.NAME,
+        capabilities = {Capability.READ, Capability.WRITE, Capability.CREATE},
+        fileSuffixes = {"json", "geojson", "topojson"},
+        resourceTypes = {FeatureSet.class})
+public class GeoJSONProvider extends DataStoreProvider {
+
+    public static final String NAME = "geojson";
+
+    public static final String ENCODING = "UTF-8";
+
+    /**
+     * The {@value} MIME type.
+     */
+    public static final String MIME_TYPE = "application/json";
+
+    private static final List<String> EXTENSIONS = Arrays.asList("json", "geojson", "topojson");
+
+    public static final ParameterDescriptor<URI> PATH = new ParameterBuilder()
+            .addName(LOCATION)
+            .addName("path")
+            .setRequired(true)
+            .create(URI.class, null);
+
+    /**
+     * Optional
+     */
+    public static final ParameterDescriptor<Integer> COORDINATE_ACCURACY = new ParameterBuilder()
+            .addName("coordinate_accuracy")
+            .addName(Bundle.formatInternational(Bundle.Keys.coordinate_accuracy))
+            .setRemarks(Bundle.formatInternational(Bundle.Keys.coordinate_accuracy_remarks))
+            .setRequired(false)
+            .create(Integer.class, 7);
+
+
+    public static final ParameterDescriptorGroup PARAMETERS_DESCRIPTOR =
+            new ParameterBuilder()
+                    .addName(NAME)
+                    .addName(Bundle.formatInternational(Bundle.Keys.datastoreTitle))
+                    .setDescription(Bundle.formatInternational(Bundle.Keys.datastoreDescription))
+                    .createGroup(PATH, COORDINATE_ACCURACY);
+
+    @Override
+    public String getShortName() {
+        return NAME;
+    }
+
+    /**
+     * {@inheritDoc }
+     */
+    @Override
+    public ParameterDescriptorGroup getOpenParameters() {
+        return PARAMETERS_DESCRIPTOR;
+    }
+
+    /**
+     * {@inheritDoc }
+     */
+    @Override
+    public GeoJSONStore open(final ParameterValueGroup params) throws DataStoreException {
+        return new GeoJSONStore(this, params);
+    }
+
+    @Override
+    public ProbeResult probeContent(StorageConnector connector) throws DataStoreException {
+        Path p = connector.getStorageAs(Path.class);
+        String extension = IOUtilities.extension(p).toLowerCase();
+        if (EXTENSIONS.contains(extension)) {
+            try {
+                final ByteBuffer buffer = connector.getStorageAs(ByteBuffer.class);
+                final Reader reader;
+                if (buffer != null) {
+                    buffer.mark();
+                    reader = null;
+                } else {
+                    // User gave us explicitely a Reader (e.g. a StringReader wrapping a String instance).
+                    reader = connector.getStorageAs(Reader.class);
+                    if (reader == null) {
+                        return ProbeResult.UNSUPPORTED_STORAGE;
+                    }
+                    reader.mark(2048); // Should be no more than {@code StorageConnector.DEFAULT_BUFFER_SIZE / 2}
+                }
+                boolean ok = false;
+                if (nextAfterSpaces(buffer, reader) == '{') {
+                    ok = true;
+                }
+                if (buffer != null) {
+                    buffer.reset();
+                } else {
+                    reader.reset();
+                }
+                if (ok) {
+                    return new ProbeResult(true, MIME_TYPE, null);
+                }
+            } catch (IOException e) {
+                throw new DataStoreException(e);
+            }
+        }
+        return ProbeResult.UNSUPPORTED_STORAGE;
+
+    }
+
+    /**
+     * Returns the next character which is not a white space, or -1 if the end of stream is reached.
+     * Exactly one of {@code buffer} and {@code reader} shall be non-null.
+     */
+    private static int nextAfterSpaces(final ByteBuffer buffer, final Reader reader) throws IOException {
+        if (buffer != null) {
+            while (buffer.hasRemaining()) {
+                final char c = (char) buffer.get();
+                if (!Character.isWhitespace(c)) {
+                    return c;
+                }
+            }
+            return -1;
+        }
+        int c;
+        while ((c = IOUtilities.readCodePoint(reader)) >= 0) {
+            if (!Character.isWhitespace(c)) break;
+        }
+        return c;
+    }
+
+    @Override
+    public DataStore open(StorageConnector connector) throws DataStoreException {
+        try {
+            final Path path = connector.getStorageAs(Path.class);
+            return new GeoJSONStore(this, path, null);
+        } catch (IllegalArgumentException ex) {
+            throw new DataStoreException(ex.getMessage(), ex);
+        }
+    }
+
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONReader.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONReader.java
new file mode 100644
index 0000000..4ab24df
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONReader.java
@@ -0,0 +1,347 @@
+/*
+ * 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.sis.storage.geojson;
+
+import org.locationtech.jts.geom.Geometry;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.storage.geojson.binding.GeoJSONFeature;
+import org.apache.sis.storage.geojson.binding.GeoJSONFeatureCollection;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry;
+import org.apache.sis.storage.geojson.binding.GeoJSONObject;
+import org.apache.sis.storage.geojson.utils.GeoJSONParser;
+import org.apache.sis.storage.geojson.utils.GeometryUtils;
+import org.apache.sis.util.ObjectConverters;
+import org.apache.sis.util.UnconvertibleObjectException;
+import org.apache.sis.util.ObjectConverter;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.nio.file.Path;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.storage.geojson.utils.GeoJSONUtils;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.opengis.feature.Attribute;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureAssociationRole;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONReader implements Iterator<Feature>, AutoCloseable {
+
+    private final static Logger LOGGER = Logging.getLogger("org.apache.sis.storage.geojson");
+    private final Map<Map.Entry<Class, Class>, ObjectConverter> convertersCache = new HashMap<>();
+
+    private GeoJSONObject jsonObj = null;
+    private Boolean toRead = true;
+
+    protected final ReadWriteLock rwlock;
+    protected final FeatureType featureType;
+    protected final Path jsonFile;
+    protected Feature current = null;
+    protected int currentFeatureIdx = 0;
+
+    /**
+     * A flag indicating if we should read identifiers from read stream. it's
+     * activated if the feature type given at built contains an {@link AttributeConvention#IDENTIFIER_PROPERTY}.
+     */
+    protected final boolean hasIdentifier;
+    final Function idConverter;
+    final CoordinateReferenceSystem crs;
+    final String geometryName;
+
+    public GeoJSONReader(Path jsonFile, FeatureType featureType, ReadWriteLock rwLock) {
+        hasIdentifier = GeoJSONUtils.hasIdentifier(featureType);
+        if (hasIdentifier) {
+            idConverter = GeoJSONUtils.getIdentifierConverter(featureType);
+        } else {
+            // It should not be used, but we don't set it to null in case someons use it by mistake.
+            idConverter = input -> input;
+        }
+
+        final PropertyType defaultGeometry = GeoJSONUtils.getDefaultGeometry(featureType);
+        crs = GeoJSONUtils.getCRS(defaultGeometry);
+        geometryName = defaultGeometry.getName().toString();
+
+        this.jsonFile = jsonFile;
+        this.featureType = featureType;
+        this.rwlock = rwLock;
+    }
+
+    public FeatureType getFeatureType() {
+        return featureType;
+    }
+
+    @Override
+    public boolean hasNext() throws BackingStoreException {
+        read();
+        return current != null;
+    }
+
+    @Override
+    public Feature next() throws BackingStoreException {
+        read();
+        final Feature ob = current;
+        current = null;
+        if(ob == null){
+            throw new BackingStoreException("No more records.");
+        }
+        return ob;
+    }
+
+    private void read() throws BackingStoreException {
+        if(current != null) return;
+
+        //first call
+        if (toRead) {
+            rwlock.readLock().lock();
+            try {
+                jsonObj = GeoJSONParser.parse(jsonFile, true);
+            } catch (IOException e) {
+                throw new BackingStoreException(e);
+            } finally {
+                toRead = false;
+                rwlock.readLock().unlock();
+            }
+        }
+
+        if (jsonObj instanceof GeoJSONFeatureCollection) {
+            final GeoJSONFeatureCollection fc = (GeoJSONFeatureCollection) jsonObj;
+            rwlock.readLock().lock();
+            try {
+                if (fc.hasNext()) {
+                    current = toFeature(fc.next());
+                    currentFeatureIdx++;
+                }
+            } finally {
+                rwlock.readLock().unlock();
+            }
+        } else if (jsonObj instanceof GeoJSONFeature) {
+            current = toFeature((GeoJSONFeature)jsonObj);
+            jsonObj = null;
+        } else if (jsonObj instanceof GeoJSONGeometry) {
+            current = toFeature((GeoJSONGeometry)jsonObj);
+            jsonObj = null;
+        }
+    }
+
+    /**
+     * Convert a GeoJSONFeature to geotk Feature.
+     * @param jsonFeature
+     * @param featureId
+     * @return
+     */
+    protected Feature toFeature(GeoJSONFeature jsonFeature) throws BackingStoreException {
+
+        //Build geometry
+        final Geometry geom = GeometryUtils.toJTS(jsonFeature.getGeometry(), crs);
+
+        //empty feature
+        final Feature feature = featureType.newInstance();
+        if (hasIdentifier) {
+            Object id = jsonFeature.getId();
+            if (id == null) {
+                id = currentFeatureIdx;
+            }
+            feature.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), idConverter.apply(id));
+        }
+        feature.setPropertyValue(geometryName, geom);
+
+        //recursively fill other properties
+        final Map<String, Object> properties = jsonFeature.getProperties();
+        fillFeature(feature, properties);
+
+        return feature;
+    }
+
+    /**
+     * Recursively fill a ComplexAttribute with properties map
+     * @param feature
+     * @param properties
+     */
+    private void fillFeature(Feature feature, Map<String, Object> properties) throws BackingStoreException {
+        final FeatureType featureType = feature.getType();
+
+        for(final PropertyType type : featureType.getProperties(true)) {
+
+            final String attName = type.getName().toString();
+            final Object value = properties.get(attName);
+            if(value==null) continue;
+
+            if (type instanceof FeatureAssociationRole ) {
+                final FeatureAssociationRole asso = (FeatureAssociationRole) type;
+                final FeatureType assoType = asso.getValueType();
+                final Class<?> valueClass = value.getClass();
+
+                if (valueClass.isArray()) {
+                    Class<?> base = value.getClass().getComponentType();
+
+                    if (!Map.class.isAssignableFrom(base)) {
+                        LOGGER.log(Level.WARNING, "Invalid complex property value " + value);
+                    }
+
+                    final int size = Array.getLength(value);
+                    if (size > 0) {
+                        //list of objects
+                        final List<Feature> subs = new ArrayList<>();
+                        for (int i = 0; i < size; i++) {
+                            final Feature subComplexAttribute = assoType.newInstance();
+                            fillFeature(subComplexAttribute, (Map) Array.get(value, i));
+                            subs.add(subComplexAttribute);
+                        }
+                        feature.setPropertyValue(attName,subs);
+                    }
+                } else if (value instanceof Map) {
+                    final Feature subComplexAttribute = assoType.newInstance();
+                    fillFeature(subComplexAttribute, (Map) value);
+                    feature.setPropertyValue(attName, subComplexAttribute);
+                }
+
+            } else if(type instanceof AttributeType) {
+                final Attribute<?> property = (Attribute<?>) feature.getProperty( type.getName().toString());
+                fillProperty(property, value);
+            }
+        }
+    }
+
+    /**
+     * Try to convert value as expected in PropertyType description.
+     * @param prop
+     * @param value
+     */
+    private void fillProperty(Attribute prop, Object value) throws BackingStoreException {
+
+        Object convertValue = null;
+        try {
+            if (value != null) {
+                final AttributeType<?> propertyType = prop.getType();
+                final Class binding = propertyType.getValueClass();
+
+                if (value.getClass().isArray() && binding.isArray()) {
+
+                    int nbdim = 1;
+                    Class base = value.getClass().getComponentType();
+                    while (base.isArray()) {
+                        base = base.getComponentType();
+                        nbdim++;
+                    }
+
+                    convertValue = rebuildArray(value, base, nbdim);
+
+                } else {
+                    convertValue = convert(value, binding);
+                }
+            }
+        } catch (UnconvertibleObjectException e1) {
+            throw new BackingStoreException(String.format("Inconvertible property %s : %s",
+                    prop.getName().tip().toString(), e1.getMessage()), e1);
+        }
+
+        prop.setValue(convertValue);
+    }
+
+    /**
+     * Rebuild nDim arrays recursively
+     * @param candidate
+     * @param componentType
+     * @param depth
+     * @return Array object
+     * @throws UnconvertibleObjectException
+     */
+    private Object rebuildArray(Object candidate, Class componentType, int depth) throws UnconvertibleObjectException {
+        if(candidate==null) return null;
+
+        if(candidate.getClass().isArray()){
+            final int size = Array.getLength(candidate);
+            final int[] dims = new int[depth];
+            dims[0] = size;
+            final Object rarray = Array.newInstance(componentType, dims);
+            depth--;
+            for(int k=0; k<size; k++){
+                Array.set(rarray, k, rebuildArray(Array.get(candidate, k), componentType, depth));
+            }
+            return rarray;
+        }else{
+            return convert(candidate, componentType);
+        }
+    }
+
+    /**
+     * Convert value object into binding class
+     * @param value
+     * @param binding
+     * @return
+     * @throws UnconvertibleObjectException
+     */
+    private Object convert(Object value, Class binding) throws UnconvertibleObjectException {
+        AbstractMap.SimpleEntry<Class, Class> key = new AbstractMap.SimpleEntry<>(value.getClass(), binding);
+        ObjectConverter converter = convertersCache.get(key);
+
+        if (converter == null) {
+            converter = ObjectConverters.find(value.getClass(), binding);
+            convertersCache.put(key, converter);
+        }
+        return converter.apply(value);
+    }
+
+    /**
+     * Convert a GeoJSONGeometry to Feature.
+     * @param jsonGeometry
+     * @return
+     */
+    protected Feature toFeature(GeoJSONGeometry jsonGeometry) {
+        final Feature feature = featureType.newInstance();
+        final Geometry geom = GeometryUtils.toJTS(jsonGeometry, crs);
+        feature.setPropertyValue(geometryName, geom);
+        return feature;
+    }
+
+    @Override
+    public void remove() {
+        throw new BackingStoreException("Not supported on reader.");
+    }
+
+    @Override
+    public void close() {
+        try {
+            // If our object is a feature collection, it could get an opened connexion to a file. We must dispose it.
+            if (jsonObj instanceof AutoCloseable) {
+                ((AutoCloseable) jsonObj).close();
+            }
+        } catch (Exception e) {
+            LOGGER.log(Level.WARNING, "Cannot close a read resource.", e);
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStore.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStore.java
new file mode 100644
index 0000000..a57b41f
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStore.java
@@ -0,0 +1,452 @@
+/*
+ * 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.sis.storage.geojson;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import org.apache.sis.feature.builder.AttributeRole;
+import org.apache.sis.feature.builder.AttributeTypeBuilder;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.internal.storage.ResourceOnFileSystem;
+import org.apache.sis.metadata.iso.DefaultMetadata;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreProvider;
+import org.apache.sis.storage.Query;
+import org.apache.sis.storage.WritableFeatureSet;
+import org.apache.sis.util.logging.Logging;
+import static org.apache.sis.storage.geojson.GeoJSONProvider.*;
+import org.apache.sis.storage.geojson.binding.*;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONGeometryCollection;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONLineString;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiLineString;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiPoint;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiPolygon;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONPoint;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONPolygon;
+import org.apache.sis.storage.geojson.utils.FeatureTypeUtils;
+import org.apache.sis.storage.geojson.utils.GeoJSONParser;
+import org.apache.sis.storage.geojson.utils.GeoJSONUtils;
+import org.apache.sis.util.iso.Names;
+import org.locationtech.jts.geom.*;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.geometry.Envelope;
+import org.opengis.metadata.Metadata;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.util.GenericName;
+
+/**
+ *
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONStore extends DataStore implements ResourceOnFileSystem, WritableFeatureSet {
+
+    private static final Logger LOGGER = Logging.getLogger("org.apache.sis.storage.geojson");
+    private static final String DESC_FILE_SUFFIX = "_Type.json";
+
+    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
+    private final DataStoreProvider provider;
+    private final ParameterValueGroup parameters;
+
+    private GenericName name;
+    private FeatureType featureType;
+    private Path descFile;
+    private Path jsonFile;
+    private Integer coordAccuracy;
+    private boolean isLocal = true;
+
+    public GeoJSONStore(DataStoreProvider provider, final Path path, Integer coordAccuracy)
+            throws DataStoreException {
+        this(provider, toParameter(path.toUri(), coordAccuracy));
+    }
+
+    public GeoJSONStore(DataStoreProvider provider, final URI uri, Integer coordAccuracy)
+            throws DataStoreException {
+        this(provider, toParameter(uri, coordAccuracy));
+    }
+
+    public GeoJSONStore (DataStoreProvider provider, final ParameterValueGroup params) throws DataStoreException {
+        super();
+        this.provider = provider;
+        this.parameters = params;
+        this.coordAccuracy = (Integer) params.parameter(COORDINATE_ACCURACY.getName().toString()).getValue();
+
+        final URI uri = (URI) params.parameter(PATH.getName().toString()).getValue();
+
+        //FIXME
+        this.isLocal = "file".equalsIgnoreCase(uri.getScheme());
+
+        Path tmpFile = null;
+        try {
+            tmpFile = Paths.get(uri);
+        } catch (FileSystemNotFoundException ex) {
+            throw new DataStoreException(ex);
+        }
+
+        final String fileName = tmpFile.getFileName().toString();
+        if (fileName.endsWith(DESC_FILE_SUFFIX)) {
+            this.descFile = tmpFile;
+            this.jsonFile = descFile.resolveSibling(fileName.replace(DESC_FILE_SUFFIX, ".json"));
+        } else {
+            this.jsonFile = tmpFile;
+            //search for description json file
+            String typeName = GeoJSONUtils.getNameWithoutExt(jsonFile);
+            this.descFile = jsonFile.resolveSibling(typeName + DESC_FILE_SUFFIX);
+        }
+    }
+
+    private static ParameterValueGroup toParameter(final URI uri, Integer coordAccuracy){
+        final Parameters params = Parameters.castOrWrap(GeoJSONProvider.PARAMETERS_DESCRIPTOR.createValue());
+        params.getOrCreate(GeoJSONProvider.PATH).setValue(uri);
+        params.getOrCreate(GeoJSONProvider.COORDINATE_ACCURACY).setValue(coordAccuracy);
+        return params;
+    }
+
+    @Override
+    public Optional<ParameterValueGroup> getOpenParameters() {
+        return Optional.of(parameters);
+    }
+
+    @Override
+    public Metadata getMetadata() throws DataStoreException {
+        return new DefaultMetadata();
+    }
+
+    @Override
+    public DataStoreProvider getProvider() {
+        return provider;
+    }
+
+    @Override
+    public Optional<GenericName> getIdentifier() throws DataStoreException{
+        checkTypeExist();
+        return Optional.of(name);
+    }
+
+    @Override
+    public FeatureType getType() throws DataStoreException{
+        checkTypeExist();
+        return featureType;
+    }
+
+    public boolean isWritable() throws DataStoreException {
+        return isLocal && Files.isWritable(descFile) && Files.isWritable(jsonFile);
+    }
+
+    private void checkTypeExist() throws DataStoreException {
+        if (name == null || featureType == null) {
+            try {
+                // try to parse file only if exist and not empty
+                if (Files.exists(jsonFile) && Files.size(jsonFile) != 0) {
+                    featureType = readType();
+                    name = featureType.getName();
+                }
+            } catch (IOException e) {
+                LOGGER.log(Level.WARNING, e.getMessage(), e);
+            }
+        }
+    }
+
+    /**
+     * Read FeatureType from a JSON-Schema file if exist or directly from the input JSON file.
+     * @return
+     * @throws DataStoreException
+     * @throws IOException
+     */
+    private FeatureType readType() throws DataStoreException, IOException {
+        if (Files.exists(descFile) && Files.size(descFile) != 0) {
+            // build FeatureType from description JSON.
+            return FeatureTypeUtils.readFeatureType(descFile);
+        } else {
+            if(Files.exists(jsonFile) && Files.size(jsonFile) != 0) {
+                final String name = GeoJSONUtils.getNameWithoutExt(jsonFile);
+
+                final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+                ftb.setName(name);
+
+                // build FeatureType from the first Feature of JSON file.
+                final GeoJSONObject obj = GeoJSONParser.parse(jsonFile, true);
+                if (obj == null) {
+                    throw new DataStoreException("Invalid GeoJSON file " + jsonFile.toString());
+                }
+
+                CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(obj);
+
+                if (obj instanceof GeoJSONFeatureCollection) {
+                    GeoJSONFeatureCollection jsonFeatureCollection = (GeoJSONFeatureCollection) obj;
+                    if (!jsonFeatureCollection.hasNext()) {
+                        //empty FeatureCollection error ?
+                        throw new DataStoreException("Empty GeoJSON FeatureCollection " + jsonFile.toString());
+                    } else {
+
+                        // TODO should we analyse all Features from FeatureCollection to be sure
+                        // that each Feature properties JSON object define exactly the same properties
+                        // with the same bindings ?
+
+                        GeoJSONFeature jsonFeature = jsonFeatureCollection.next();
+                        fillTypeFromFeature(ftb, crs, jsonFeature, false);
+                    }
+
+                } else if (obj instanceof GeoJSONFeature) {
+                    GeoJSONFeature jsonFeature = (GeoJSONFeature) obj;
+                    fillTypeFromFeature(ftb, crs, jsonFeature, true);
+                } else if (obj instanceof GeoJSONGeometry) {
+                    ftb.addAttribute(String.class).setName("fid").addRole(AttributeRole.IDENTIFIER_COMPONENT);
+                    ftb.addAttribute(findBinding((GeoJSONGeometry) obj)).setName("geometry").setCRS(crs).addRole(AttributeRole.DEFAULT_GEOMETRY);
+                }
+
+                return ftb.build();
+            } else {
+                throw new DataStoreException("Can't create FeatureType from empty/not found Json file "+jsonFile.getFileName().toString());
+            }
+        }
+    }
+
+    private void fillTypeFromFeature(FeatureTypeBuilder ftb, CoordinateReferenceSystem crs,
+                                     GeoJSONFeature jsonFeature, boolean analyseGeometry) {
+        if (analyseGeometry) {
+            ftb.addAttribute(findBinding(jsonFeature.getGeometry())).setName("geometry").setCRS(crs).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        } else {
+            ftb.addAttribute(Geometry.class).setName("geometry").setCRS(crs).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        }
+        for (Map.Entry<String, Object> property : jsonFeature.getProperties().entrySet()) {
+            final Object value = property.getValue();
+            final Class<?> binding = value != null ? value.getClass() : String.class;
+            final GenericName name = Names.createLocalName(null, null, property.getKey());
+            final AttributeTypeBuilder<?> atb = ftb.addAttribute(binding).setName(name);
+            if ("id".equals(property.getKey()) || "fid".equals(property.getKey())) {
+                atb.addRole(AttributeRole.IDENTIFIER_COMPONENT);
+            }
+        }
+    }
+
+    private Class<? extends Geometry> findBinding(GeoJSONGeometry jsonGeometry) {
+
+        if (jsonGeometry instanceof GeoJSONPoint) {
+            return Point.class;
+        } else if (jsonGeometry instanceof GeoJSONLineString) {
+            return LineString.class;
+        } else if (jsonGeometry instanceof GeoJSONPolygon) {
+            return Polygon.class;
+        } else if (jsonGeometry instanceof GeoJSONMultiPoint) {
+            return MultiPoint.class;
+        } else if (jsonGeometry instanceof GeoJSONMultiLineString) {
+            return MultiLineString.class;
+        } else if (jsonGeometry instanceof GeoJSONMultiPolygon) {
+            return MultiPolygon.class;
+        } else if (jsonGeometry instanceof GeoJSONGeometryCollection) {
+            return GeometryCollection.class;
+        } else {
+            throw new IllegalArgumentException("Unsupported geometry type : " + jsonGeometry);
+        }
+
+    }
+
+    private void writeType(FeatureType featureType) throws DataStoreException {
+        try {
+            final boolean jsonExist = Files.exists(jsonFile);
+
+            if (jsonExist && Files.size(jsonFile) != 0) {
+                throw new DataStoreException(String.format("Non empty json file %s can't create new json file %s",
+                        jsonFile.getFileName().toString(), featureType.getName()));
+            }
+
+            if (!jsonExist) Files.createFile(jsonFile);
+            //create json with empty collection
+            GeoJSONUtils.writeEmptyFeatureCollection(jsonFile);
+
+            //json schema file
+            final boolean descExist = Files.exists(descFile);
+
+            if (descExist && Files.size(descFile) != 0) {
+                throw new DataStoreException(String.format("Non empty json schema file %s can't create new json schema %s",
+                        descFile.getFileName().toString(), featureType.getName()));
+            }
+
+            if (!descExist) Files.createFile(descFile);
+            //create json schema file
+            FeatureTypeUtils.writeFeatureType(featureType, descFile);
+
+            this.featureType = featureType;
+            this.name = featureType.getName();
+        } catch (IOException e) {
+            throw new DataStoreException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public Optional<Envelope> getEnvelope() throws DataStoreException {
+        Envelope envelope = null;
+        rwLock.readLock().lock();
+        try {
+            final GeoJSONObject obj = GeoJSONParser.parse(jsonFile, true);
+            final CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(obj);
+            envelope = GeoJSONUtils.getEnvelope(obj, crs);
+        } catch (IOException e) {
+            throw new DataStoreException(e.getMessage(), e);
+        } finally {
+            rwLock.readLock().unlock();
+        }
+
+        return Optional.ofNullable(envelope);
+    }
+
+    /**
+     * {@inheritDoc }
+     */
+    @Override
+    public Stream<Feature> features(boolean parallel) throws DataStoreException {
+        final GeoJSONReader reader = new GeoJSONReader(jsonFile, featureType, rwLock);
+        final Stream<Feature> stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(reader, Spliterator.ORDERED), false);
+        stream.onClose(reader::close);
+        return stream;
+    }
+
+    @Override
+    public void add(Iterator<? extends Feature> features) throws DataStoreException {
+        try (GeoJSONFileWriter writer = getFeatureWriter()) {
+            //rewrite existing features
+            while (writer.hasNext()) {
+                writer.next();
+            }
+            //new features
+            while (features.hasNext()) {
+                Feature feature = features.next();
+                Feature next = writer.next();
+                writer.write(feature);
+            }
+        }
+    }
+
+    @Override
+    public boolean removeIf(Predicate<? super Feature> filter) throws DataStoreException {
+        boolean modified = false;
+        try (GeoJSONFileWriter writer = getFeatureWriter()) {
+            //rewrite existing features
+            while (writer.hasNext()) {
+                Feature feature = writer.next();
+                if (filter.test(feature)) {
+                    writer.remove();
+                    modified = true;
+                }
+            }
+        }
+        return modified;
+    }
+
+    @Override
+    public void replaceIf(Predicate<? super Feature> filter, UnaryOperator<Feature> updater) throws DataStoreException {
+        try (GeoJSONFileWriter writer = getFeatureWriter()) {
+            //rewrite existing features
+            while (writer.hasNext()) {
+                Feature feature = writer.next();
+                if (filter.test(feature)) {
+                    Feature changed = updater.apply(feature);
+                    if (changed == null) {
+                        writer.remove();
+                    } else {
+                        writer.write();
+                    }
+                }
+            }
+        }
+    }
+
+    private GeoJSONFileWriter getFeatureWriter() throws DataStoreException {
+        return new GeoJSONFileWriter(jsonFile, featureType, rwLock,
+                GeoJSONProvider.ENCODING, coordAccuracy);
+    }
+
+    @Override
+    public void updateType(final FeatureType featureType) throws DataStoreException {
+        if (!isLocal) {
+            throw new DataStoreException("Cannot create FeatureType on remote GeoJSON");
+        }
+        GenericName typeName = featureType.getName();
+        if (typeName == null) {
+            throw new DataStoreException("Type name can not be null.");
+        }
+        if (!typeName.tip().toString().equals(GeoJSONUtils.getNameWithoutExt(jsonFile))) {
+            throw new DataStoreException("New type name should be equals to file name.");
+        }
+
+        //delete previous files
+        rwLock.writeLock().lock();
+        try {
+            Files.deleteIfExists(descFile);
+            Files.deleteIfExists(jsonFile);
+            Files.createFile(jsonFile);
+        } catch (IOException e) {
+            throw new DataStoreException("Can not delete GeoJSON schema.", e);
+        } finally {
+            rwLock.writeLock().unlock();
+        }
+
+        //create new type
+        rwLock.writeLock().lock();
+        try {
+            writeType(featureType);
+        } finally {
+            rwLock.writeLock().unlock();
+        }
+    }
+
+    public void clearCache() {
+        name = null;
+        featureType = null;
+    }
+
+    @Override
+    public void close() throws DataStoreException {
+    }
+
+    /**
+     * {@inheritDoc }
+     */
+    @Override
+    public Path[] getComponentFiles() throws DataStoreException {
+        List<Path> files = new ArrayList<>();
+        if (Files.exists(jsonFile)) {
+            files.add(jsonFile);
+        }
+        if (Files.exists(descFile)) {
+            files.add(descFile);
+        }
+        return files.toArray(new Path[files.size()]);
+    }
+
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStreamWriter.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStreamWriter.java
new file mode 100644
index 0000000..f43a42d
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStreamWriter.java
@@ -0,0 +1,221 @@
+/*
+ * 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.sis.storage.geojson;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import org.locationtech.jts.geom.Geometry;
+import org.apache.sis.storage.DataStoreException;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.feature.builder.PropertyTypeBuilder;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.storage.FeatureNaming;
+import org.apache.sis.storage.IllegalNameException;
+import org.apache.sis.storage.geojson.utils.GeoJSONUtils;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.Operation;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since 2.0
+ * @module
+ */
+public class GeoJSONStreamWriter implements Iterator<Feature>, AutoCloseable {
+
+    private final GeoJSONWriter writer;
+    private final FeatureType featureType;
+
+    private Feature edited = null;
+    private Feature lastWritten = null;
+    private int currentFeatureIdx = 0;
+
+    private final boolean hasIdentifier;
+
+    final Function idConverter;
+
+    /**
+     *
+     * @param outputStream stream were GeoJSON will be written
+     * @param featureType {@link FeatureType} of features to write.
+     * @param doubleAccuracy number of coordinates fraction digits
+     * @throws DataStoreException
+     */
+    public GeoJSONStreamWriter(OutputStream outputStream, FeatureType featureType, final int doubleAccuracy)
+            throws DataStoreException {
+        this(outputStream, featureType, JsonEncoding.UTF8, doubleAccuracy);
+    }
+
+    /**
+     *
+     * @param outputStream stream were GeoJSON will be written
+     * @param featureType {@link FeatureType} of features to write.
+     * @param encoding character encoding
+     * @param doubleAccuracy number of coordinates fraction digits
+     * @throws DataStoreException
+     */
+    public GeoJSONStreamWriter(OutputStream outputStream, FeatureType featureType, final JsonEncoding encoding, final int doubleAccuracy)
+            throws DataStoreException {
+        this(outputStream, featureType, encoding, doubleAccuracy, false);
+    }
+
+    public GeoJSONStreamWriter(OutputStream outputStream, FeatureType featureType, final JsonEncoding encoding, final int doubleAccuracy, boolean prettyPrint)
+            throws DataStoreException {
+
+        //remove any operation attribute
+        List<Operation> geometries = featureType.getProperties(true).stream()
+                .filter(Operation.class::isInstance)
+                .map(Operation.class::cast)
+                .filter(AttributeConvention::isGeometryAttribute)
+                .filter(GeoJSONUtils.IS_NOT_CONVENTION)
+                .collect(Collectors.toList());
+
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder(featureType);
+        final Iterator<PropertyTypeBuilder> it = ftb.properties().iterator();
+        final FeatureNaming naming = new FeatureNaming();
+        geometries.stream()
+                .map(Operation::getName)
+                .forEach(name -> {
+                    try {
+                        naming.add(null, name, name);
+                    } catch (IllegalNameException e) {
+                        //hack
+                    }
+                });
+        while (it.hasNext()) {
+            try {
+                naming.get(null, it.next().getName().toString());
+                it.remove();
+            } catch (IllegalNameException e) {
+                // normal behavior
+            }
+        }
+
+        for (final Operation op : geometries) {
+            GeoJSONUtils.castOrUnwrap(op).ifPresent(ftb::addAttribute);
+        }
+
+        this.featureType = ftb.build();
+        hasIdentifier = GeoJSONUtils.hasIdentifier(featureType);
+        if (hasIdentifier) {
+            idConverter = GeoJSONUtils.getIdentifierConverter(featureType);
+        } else {
+            // It should not be used, but we don't set it to null in case someons use it by mistake.
+            idConverter = input -> input;
+        }
+
+        try {
+            writer = new GeoJSONWriter(outputStream, encoding, doubleAccuracy, prettyPrint);
+            //start write feature collection.
+            writer.writeStartFeatureCollection(GeoJSONUtils.getCRS(featureType), null);
+            writer.flush();
+        } catch (IOException ex) {
+            throw new DataStoreException(ex.getMessage(), ex);
+        }
+    }
+
+    /**
+     * Utility method to write a single Feature into an OutputStream
+     *
+     * @param outputStream
+     * @param feature to write
+     * @param encoding
+     * @param doubleAccuracy
+     * @param prettyPrint
+     */
+    public static void writeSingleFeature(OutputStream outputStream, Feature feature, final JsonEncoding encoding,
+            final int doubleAccuracy, boolean prettyPrint) throws IOException {
+
+        try (final GeoJSONWriter writer = new GeoJSONWriter(outputStream, encoding, doubleAccuracy, prettyPrint)) {
+            writer.writeSingleFeature(feature);
+        }
+    }
+
+    /**
+     * Utility method to write a single Geometry into an OutputStream
+     *
+     * @param outputStream
+     * @param geometry to write
+     * @param encoding
+     * @param doubleAccuracy
+     * @param prettyPrint
+     */
+    public static void writeSingleGeometry(OutputStream outputStream, Geometry geometry, final JsonEncoding encoding,
+            final int doubleAccuracy, boolean prettyPrint) throws IOException {
+
+        try (final GeoJSONWriter writer = new GeoJSONWriter(outputStream, encoding, doubleAccuracy, prettyPrint)) {
+            writer.writeSingleGeometry(geometry);
+        }
+    }
+
+    public FeatureType getFeatureType() {
+        return featureType;
+    }
+
+    @Override
+    public Feature next() throws BackingStoreException {
+        edited = featureType.newInstance();
+        if (hasIdentifier) {
+            edited.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), idConverter.apply(currentFeatureIdx++));
+        }
+        return edited;
+    }
+
+    @Override
+    public void remove() throws BackingStoreException {
+        throw new BackingStoreException("Not supported on reader.");
+    }
+
+    public void write() throws BackingStoreException {
+        if (edited == null || edited.equals(lastWritten)) {
+            return;
+        }
+
+        lastWritten = edited;
+        try {
+            writer.writeFeature(edited);
+            writer.flush();
+        } catch (IOException | IllegalArgumentException e) {
+            throw new BackingStoreException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public boolean hasNext() throws BackingStoreException {
+        return true;
+    }
+
+    @Override
+    public void close() {
+        try {
+            writer.writeEndFeatureCollection();
+            writer.flush();
+            writer.close();
+        } catch (IOException ex) {
+            throw new BackingStoreException(ex);
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONWriter.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONWriter.java
new file mode 100644
index 0000000..72aac30
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONWriter.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.sis.storage.geojson;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonGenerator;
+import org.locationtech.jts.geom.Geometry;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry;
+import org.apache.sis.storage.geojson.utils.GeoJSONParser;
+import org.apache.sis.storage.geojson.utils.GeoJSONUtils;
+import org.apache.sis.storage.geojson.utils.GeometryUtils;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.NumberFormat;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.nio.file.StandardOpenOption.WRITE;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.util.Utilities;
+import static org.apache.sis.storage.geojson.utils.GeoJSONMembres.*;
+import static org.apache.sis.storage.geojson.utils.GeoJSONTypes.*;
+import static org.apache.sis.storage.geojson.binding.GeoJSONGeometry.*;
+import org.opengis.feature.Attribute;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureAssociationRole;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.Operation;
+import org.opengis.feature.PropertyNotFoundException;
+import org.opengis.feature.PropertyType;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+class GeoJSONWriter implements Closeable, Flushable {
+
+    private final static String SYS_LF;
+    static {
+        String lf = null;
+        try {
+            lf = System.getProperty("line.separator");
+        } catch (Throwable t) { } // access exception?
+        SYS_LF = (lf == null) ? "\n" : lf;
+    }
+
+    private final JsonGenerator writer;
+    private final OutputStream outputStream;
+    private boolean first = true;
+    private boolean prettyPrint = true;
+
+    // state boolean to ensure that we can't call writeStartFeatureCollection
+    // if we first called writeSingleFeature
+    private boolean isFeatureCollection = false;
+    private boolean isSingleFeature = false;
+    private boolean isSingleGeometry = false;
+
+    private final NumberFormat numberFormat;
+
+    @Deprecated
+    GeoJSONWriter(File file, JsonEncoding encoding, int doubleAccuracy, boolean prettyPrint) throws IOException {
+        this(file.toPath(), encoding, doubleAccuracy, prettyPrint);
+    }
+
+    GeoJSONWriter(Path file, JsonEncoding encoding, int doubleAccuracy, boolean prettyPrint) throws IOException {
+        this(Files.newOutputStream(file, CREATE, WRITE, TRUNCATE_EXISTING), encoding, doubleAccuracy, prettyPrint);
+    }
+
+    GeoJSONWriter(OutputStream stream, JsonEncoding encoding,  int doubleAccuracy, boolean prettyPrint) throws IOException {
+        this.prettyPrint = prettyPrint;
+        this.outputStream = null;
+        if (prettyPrint) {
+            this.writer = GeoJSONParser.FACTORY.createGenerator(stream, encoding).useDefaultPrettyPrinter();
+        } else {
+            this.writer = GeoJSONParser.FACTORY.createGenerator(stream, encoding);
+        }
+
+        this.writer.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, true);
+        numberFormat = NumberFormat.getInstance(Locale.US);
+        numberFormat.setGroupingUsed(false);
+        numberFormat.setMaximumFractionDigits(doubleAccuracy);
+    }
+
+
+    void writeStartFeatureCollection(CoordinateReferenceSystem crs, Envelope envelope) throws IOException {
+
+        assert(!isFeatureCollection && !isSingleFeature && !isSingleGeometry) :
+                "Can't write FeatureCollection if we start a single feature or geometry GeoJSON.";
+        isFeatureCollection = true;
+
+        writer.writeStartObject();
+        writeNewLine();
+
+        writer.writeStringField(TYPE, FEATURE_COLLECTION);
+        writeNewLine();
+
+        if (crs != null && !Utilities.equalsApproximately(crs, CommonCRS.defaultGeographic())) {
+            if (writeCRS(crs)) {
+                writeNewLine();
+            } else {
+                throw new IOException("Cannot determine a valid URN for "+crs.getName());
+            }
+        }
+
+        if (envelope != null) {
+            //TODO write bbox
+            writeNewLine();
+        }
+    }
+
+    void writeEndFeatureCollection() throws IOException {
+        assert(isFeatureCollection && !isSingleFeature && !isSingleGeometry) :
+                "Can't write FeatureCollection end before writeStartFeatureCollection().";
+
+        if (!first) {
+            writer.writeEndArray(); //close feature collection array
+        }
+        writer.writeEndObject(); //close root object
+    }
+
+    /**
+     * Write GeoJSON with a single feature
+     * @param feature
+     * @throws IOException
+     * @throws IllegalArgumentException
+     */
+    void writeSingleFeature(Feature feature) throws IOException, IllegalArgumentException {
+        assert(!isFeatureCollection && !isSingleFeature && !isSingleGeometry) :
+                "writeSingleFeature can called only once per GeoJSONWriter.";
+
+        isSingleFeature = true;
+        writeFeature(feature, true);
+    }
+
+    void writeFeature(Feature feature) throws IOException, IllegalArgumentException {
+        assert(isFeatureCollection && !isSingleFeature && !isSingleGeometry) :
+                "Can't write a Feature before writeStartFeatureCollection.";
+        writeFeature(feature, false);
+    }
+
+    /**
+     * Write a Feature.
+     * @param feature
+     * @param single
+     * @throws IOException
+     * @throws IllegalArgumentException
+     */
+    private void writeFeature(Feature feature, boolean single) throws IOException, IllegalArgumentException {
+        if (!single) {
+            if (first) {
+                writer.writeArrayFieldStart(FEATURES);
+                writeNewLine();
+                first = false;
+            }
+        }
+
+        writer.writeStartObject();
+        writer.writeStringField(TYPE, FEATURE);
+        /* As defined in GeoJSON spec, identifier is an optional attribute. For
+         * more details, see https://tools.ietf.org/html/rfc7946#section-3.2
+         */
+        try {
+            final Object idValue = feature.getPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString());
+            // TODO : search for a property named id or identifier ?
+            if (idValue != null) {
+                writeAttribute(ID, idValue, true);
+            }
+        } catch (PropertyNotFoundException e) {
+            GeoJSONParser.LOGGER.log(Level.FINE, "Cannot write ID cause no matching property has been found.", e);
+        }
+
+        //write CRS
+        if (single) {
+            final CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(feature.getType());
+            if (crs != null && !Utilities.equalsApproximately(crs, CommonCRS.defaultGeographic())) {
+                if (!writeCRS(crs)) {
+                    throw new IOException("Cannot determine a valid URN for "+crs.getName());
+                }
+            }
+        }
+
+        //write geometry
+        final Optional<Geometry> geom = GeoJSONUtils.getDefaultGeometryValue(feature)
+                        .filter(Geometry.class::isInstance)
+                        .map(Geometry.class::cast);
+
+        if (geom.isPresent()) {
+            writer.writeFieldName(GEOMETRY);
+            writeFeatureGeometry(geom.get());
+        }
+
+        //write properties
+        writeProperties(feature, PROPERTIES, true);
+        writer.writeEndObject();
+
+        if (!single && !prettyPrint) writer.writeRaw(SYS_LF);
+    }
+
+    private void writeNewLine() throws IOException {
+        if (!prettyPrint) writer.writeRaw(SYS_LF);
+    }
+
+    /**
+     * Write CoordinateReferenceSystem
+     * @param crs
+     * @throws IOException
+     */
+    private boolean writeCRS(CoordinateReferenceSystem crs) throws IOException {
+        final Optional<String> urn = GeoJSONUtils.toURN(crs);
+        if (urn.isPresent()) {
+            writer.writeObjectFieldStart(CRS);
+            writer.writeStringField(TYPE, CRS_NAME);
+            writer.writeObjectFieldStart(PROPERTIES);
+            writer.writeStringField(NAME, urn.get());
+            writer.writeEndObject();//close properties
+            writer.writeEndObject();//close crs
+        }
+
+        return urn.isPresent();
+    }
+
+    /**
+     * Write ComplexAttribute.
+     * @param edited
+     * @param fieldName
+     * @param writeFieldName
+     * @throws IOException
+     * @throws IllegalArgumentException
+     */
+    private void writeProperties(Feature edited, String fieldName, boolean writeFieldName)
+            throws IOException, IllegalArgumentException {
+        if (writeFieldName) {
+            writer.writeObjectFieldStart(fieldName);
+        } else {
+            writer.writeStartObject();
+        }
+
+        FeatureType type = edited.getType();
+        Collection<? extends PropertyType> descriptors = type.getProperties(true).stream()
+                .filter(GeoJSONUtils.IS_NOT_CONVENTION)
+                .collect(Collectors.toList());
+        for (PropertyType propType : descriptors) {
+            if(AttributeConvention.contains(propType.getName())) continue;
+            if(AttributeConvention.isGeometryAttribute(propType)) continue;
+            final String name = propType.getName().tip().toString();
+            final Object value = edited.getPropertyValue(propType.getName().toString());
+
+            if(propType instanceof AttributeType){
+                final AttributeType attType = (AttributeType) propType;
+                if(attType.getMaximumOccurs()>1){
+                    writer.writeArrayFieldStart(name);
+                    for(Object v : (Collection)value){
+                        writeProperty(name, v, false);
+                    }
+                    writer.writeEndArray();
+                }else{
+                    writeProperty(name, value, true);
+                }
+            }else if(propType instanceof FeatureAssociationRole){
+                final FeatureAssociationRole asso = (FeatureAssociationRole) propType;
+                if(asso.getMaximumOccurs()>1){
+                    writer.writeArrayFieldStart(name);
+                    for(Object v : (Collection)value){
+                        writeProperty(name, v, false);
+                    }
+                    writer.writeEndArray();
+                }else{
+                    writeProperty(name, value, true);
+                }
+
+            }else if(propType instanceof Operation){
+                writeProperty(name, value, true);
+            }
+        }
+
+        writer.writeEndObject();
+    }
+
+    /**
+     * Write a property (Complex or Simple)
+     * @param property
+     * @param writeFieldName
+     * @throws IOException
+     */
+    private void writeProperty(String name, Object value, boolean writeFieldName) throws IOException, IllegalArgumentException {
+        if (value instanceof Feature) {
+            writeProperties((Feature) value, name, writeFieldName);
+        } else {
+            writeAttribute(name, value, writeFieldName);
+        }
+    }
+
+    /**
+     * Write an Attribute and check if attribute value is assignable to binding class.
+     * @param property
+     * @param writeFieldName
+     * @throws IOException
+     */
+    private void writeAttribute(String name, Object value,  boolean writeFieldName) throws IOException, IllegalArgumentException {
+        if (writeFieldName) {
+            writer.writeFieldName(name);
+        }
+        GeoJSONUtils.writeValue(value, writer);
+    }
+
+    /**
+     * Write a GeometryAttribute
+     * @param geom
+     * @throws IOException
+     */
+    void writeSingleGeometry(Attribute geom) throws IOException {
+        assert(!isFeatureCollection && !isSingleFeature && !isSingleGeometry) :
+                "writeSingleGeometry can called only once per GeoJSONWriter.";
+        isSingleGeometry = true;
+        GeoJSONGeometry jsonGeometry = GeometryUtils.toGeoJSONGeometry((Geometry) geom.getValue());
+        writeGeoJSONGeometry(jsonGeometry);
+    }
+
+    /**
+     * Write a JTS Geometry
+     * @param geom
+     * @throws IOException
+     */
+    void writeSingleGeometry(Geometry geom) throws IOException {
+        assert(!isFeatureCollection && !isSingleFeature && !isSingleGeometry) :
+                "writeSingleGeometry can called only once per GeoJSONWriter.";
+        isSingleGeometry = true;
+        GeoJSONGeometry jsonGeometry = GeometryUtils.toGeoJSONGeometry(geom);
+        writeGeoJSONGeometry(jsonGeometry);
+    }
+
+    /**
+     * Write a GeometryAttribute
+     * @param geom
+     * @throws IOException
+     */
+    private void writeFeatureGeometry(Geometry geom) throws IOException {
+        writeGeoJSONGeometry(GeometryUtils.toGeoJSONGeometry(geom));
+    }
+
+    /**
+     * Write a GeoJSONGeometry
+     * @param jsonGeometry
+     * @throws IOException
+     */
+    private void writeGeoJSONGeometry(GeoJSONGeometry jsonGeometry) throws IOException {
+        writer.writeStartObject();
+        writer.writeStringField(TYPE, jsonGeometry.getType());
+
+        if (jsonGeometry instanceof GeoJSONGeometryCollection) {
+            List<GeoJSONGeometry> geometries = ((GeoJSONGeometryCollection) jsonGeometry).getGeometries();
+            writer.writeArrayFieldStart(GEOMETRIES); // "geometries" : [
+            for (GeoJSONGeometry geometry : geometries) {
+                writeGeoJSONGeometry(geometry);
+            }
+            writer.writeEndArray(); // "]"
+        } else {
+            writer.writeArrayFieldStart(COORDINATES); // "coordinates" : [
+            if (jsonGeometry instanceof GeoJSONPoint) {
+                writeArray(((GeoJSONPoint) jsonGeometry).getCoordinates());
+            } else if (jsonGeometry instanceof GeoJSONLineString) {
+                writeArray(((GeoJSONLineString) jsonGeometry).getCoordinates());
+            } else if (jsonGeometry instanceof GeoJSONPolygon) {
+                writeArray(((GeoJSONPolygon) jsonGeometry).getCoordinates());
+            } else if (jsonGeometry instanceof GeoJSONMultiPoint) {
+                writeArray(((GeoJSONMultiPoint) jsonGeometry).getCoordinates());
+            } else if (jsonGeometry instanceof GeoJSONMultiLineString) {
+                writeArray(((GeoJSONMultiLineString) jsonGeometry).getCoordinates());
+            } else if (jsonGeometry instanceof GeoJSONMultiPolygon) {
+                writeArray(((GeoJSONMultiPolygon) jsonGeometry).getCoordinates());
+            } else {
+                throw new IllegalArgumentException("Unsupported geometry type : " + jsonGeometry);
+            }
+            writer.writeEndArray(); // "]"
+        }
+
+        writer.writeEndObject();
+    }
+
+    private void writeArray(double[] coordinates) throws IOException {
+        for (double coordinate : coordinates) {
+            writer.writeNumber(numberFormat.format(coordinate));
+        }
+    }
+
+    private void writeArray(double[][] coordinates) throws IOException {
+        for (double[] coordinate : coordinates) {
+            writer.writeStartArray(); // "["
+            writeArray(coordinate);
+            writer.writeEndArray(); // "]"
+        }
+    }
+
+    private void writeArray(double[][][] coordinates) throws IOException {
+        for (double[][] coordinate : coordinates) {
+            writer.writeStartArray(); // "["
+            writeArray(coordinate);
+            writer.writeEndArray(); // "]"
+        }
+    }
+
+    private void writeArray(double[][][][] coordinates) throws IOException {
+        for (double[][][] coordinate : coordinates) {
+            writer.writeStartArray(); // "["
+            writeArray(coordinate);
+            writer.writeEndArray(); // "]"
+        }
+    }
+
+    @Override
+    public void flush() throws IOException {
+        if (writer != null) {
+            writer.flush();
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (writer != null) {
+            writer.close();
+        }
+        if (outputStream != null) {
+            outputStream.close();
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONCRS.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONCRS.java
new file mode 100644
index 0000000..cd118c0
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONCRS.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.sis.storage.geojson.binding;
+
+import java.io.Serializable;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.sis.referencing.crs.AbstractCRS;
+import org.apache.sis.referencing.cs.AxesConvention;
+import static org.apache.sis.storage.geojson.utils.GeoJSONMembres.*;
+import static org.apache.sis.storage.geojson.utils.GeoJSONTypes.*;
+import org.apache.sis.storage.geojson.utils.GeoJSONUtils;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.util.FactoryException;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONCRS implements Serializable {
+
+    private String type;
+    private final Map<String, String> properties = new HashMap<>();
+
+    public GeoJSONCRS() {
+    }
+
+    public GeoJSONCRS(CoordinateReferenceSystem crs) {
+        type = CRS_NAME;
+        setCRS(crs);
+    }
+
+    public GeoJSONCRS(URL url, String crsType) {
+        type = CRS_LINK;
+        if (url != null && crsType != null) {
+            properties.put(HREF, url.toString());
+            properties.put(TYPE, crsType);
+        }
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    public CoordinateReferenceSystem getCRS() throws FactoryException, MalformedURLException {
+        if (type.equals(CRS_NAME)) {
+            String name = properties.get(NAME);
+            CoordinateReferenceSystem crs = org.apache.sis.referencing.CRS.forCode(name);
+            if (!name.startsWith("urn")) {
+                //legacy names, we force longitude first for those
+                crs = AbstractCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED);
+            }
+            return crs;
+        } else if (type.equals(CRS_LINK)) {
+            final String href = properties.get(HREF);
+            final String crsType = properties.get(TYPE);
+            return GeoJSONUtils.parseCRS(href, crsType);
+        }
+        return null;
+    }
+
+    public void setCRS(CoordinateReferenceSystem crs) {
+        type = CRS_NAME;
+        GeoJSONUtils.toURN(crs)
+                .ifPresent(urn -> properties.put(NAME, urn));
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONFeature.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONFeature.java
new file mode 100644
index 0000000..f15d589
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONFeature.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.sis.storage.geojson.binding;
+
+import org.apache.sis.storage.geojson.utils.GeoJSONTypes;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONFeature extends GeoJSONObject {
+
+    private GeoJSONGeometry geometry = null;
+    /**
+     * Identifier (id attribute) of the feature. According to RFC 7946, it is
+     * optional and can either be a number or a string.
+     */
+    private Object id = null;
+    private Map<String, Object> properties = new HashMap<>();
+
+    public GeoJSONFeature() {
+        setType(GeoJSONTypes.FEATURE);
+    }
+
+    public GeoJSONGeometry getGeometry() {
+        return geometry;
+    }
+
+    public void setGeometry(GeoJSONGeometry geometry) {
+        this.geometry = geometry;
+    }
+
+    public Object getId() {
+        return id;
+    }
+
+    public void setId(Object id) {
+        this.id = id;
+    }
+
+    public Map<String, Object> getProperties() {
+        return properties;
+    }
+
+    public void setProperties(Map<String, Object> properties) {
+        this.properties = properties;
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONFeatureCollection.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONFeatureCollection.java
new file mode 100644
index 0000000..a563cde
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONFeatureCollection.java
@@ -0,0 +1,189 @@
+/*
+ * 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.sis.storage.geojson.binding;
+
+import com.fasterxml.jackson.core.JsonLocation;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+
+import org.apache.sis.storage.geojson.utils.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.sis.util.collection.BackingStoreException;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONFeatureCollection extends GeoJSONObject implements GeoJSONFeatureIterator<GeoJSONFeature> {
+
+    private List<GeoJSONFeature> features = new ArrayList<>();
+
+    transient JsonLocation currentPos = null;
+    transient GeoJSONFeature current = null;
+    transient int currentIdx = 0;
+    transient InputStream readStream;
+    transient JsonParser parser;
+
+    /**
+     * If current GeoJSONFeatureCollection is in lazy parsing mode,
+     * sourceInput should be not {@code null} and used to create {@link JsonParser object}
+     */
+    transient Path sourceInput = null;
+    transient LiteJsonLocation startPos = null;
+    transient LiteJsonLocation endPos = null;
+    transient Boolean lazyMode;
+
+    public GeoJSONFeatureCollection(Boolean lazyMode) {
+        setType(GeoJSONTypes.FEATURE_COLLECTION);
+        this.lazyMode = lazyMode;
+    }
+
+    public List<GeoJSONFeature> getFeatures() {
+        return features;
+    }
+
+    public void setFeatures(List<GeoJSONFeature> features) {
+        this.features = features;
+    }
+
+    public void setStartPosition(JsonLocation startPos) {
+        this.startPos = new LiteJsonLocation(startPos);
+    }
+
+    public void setEndPosition(JsonLocation endPos) {
+        this.endPos = new LiteJsonLocation(endPos);
+    }
+
+    public void setSourceInput(Path input) {
+        this.sourceInput = input;
+    }
+
+    public Boolean isLazyMode() {
+        return lazyMode;
+    }
+
+    @Override
+    public boolean hasNext() {
+        try {
+            findNext();
+            return current != null;
+        } catch (IOException e) {
+            throw new BackingStoreException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public GeoJSONFeature next() {
+        try {
+            findNext();
+            final GeoJSONFeature ob = current;
+            current = null;
+            if (ob == null) {
+                throw new BackingStoreException("No more feature.");
+            }
+            return ob;
+        } catch (IOException e) {
+            throw new BackingStoreException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Find next Feature from features list or as lazy parsing.
+     * @throws IOException
+     */
+    private void findNext() throws IOException {
+        if (current != null) return;
+        if (lazyMode) {
+            if (sourceInput == null || startPos == null || endPos == null) return;
+
+            if (parser == null) {
+                readStream = Files.newInputStream(sourceInput);
+                parser = GeoJSONParser.FACTORY.createParser(readStream);
+            }
+
+            //loop to FeatureCollection start
+            if (currentPos == null) {
+                while (!startPos.equals(currentPos)) {
+                    parser.nextToken();
+                    currentPos = parser.getCurrentLocation();
+                }
+            }
+
+            current = null;
+
+            // set parser to feature object start
+            while (parser.getCurrentToken() != JsonToken.START_OBJECT && !endPos.equals(currentPos)) {
+
+                if (parser.getCurrentToken() != JsonToken.START_OBJECT && endPos.isBefore(currentPos)) {
+                    //cannot find collection end token and no more start object token
+                    //break loop to avoid infinite search
+                    break;
+                }
+                parser.nextToken();
+                currentPos = parser.getCurrentLocation();
+            }
+
+            if (!endPos.equals(currentPos)) {
+                GeoJSONObject obj = GeoJSONParser.parseGeoJSONObject(parser);
+                if (obj instanceof GeoJSONFeature) {
+                    current = (GeoJSONFeature) obj;
+                }
+                currentPos = parser.getCurrentLocation();
+            }
+        } else {
+            if (currentIdx < features.size()) {
+                current = features.get(currentIdx++);
+            } else {
+                current = null;
+            }
+        }
+    }
+
+    @Override
+    public void remove() {
+        //do nothing
+    }
+
+    @Override
+    public void close() {
+        //close read stream
+        if (readStream != null) {
+            try {
+                readStream.close();
+            } catch (IOException e) {
+                throw new BackingStoreException(e.getMessage(), e);
+            }
+        }
+        //close parser
+        if (parser != null && !parser.isClosed()) {
+            try {
+                parser.close();
+            } catch (IOException e) {
+                throw new BackingStoreException(e.getMessage(), e);
+            }
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONGeometry.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONGeometry.java
new file mode 100644
index 0000000..4c0a1db
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONGeometry.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.sis.storage.geojson.binding;
+
+import org.apache.sis.storage.geojson.utils.GeoJSONTypes;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONGeometry extends GeoJSONObject implements Serializable {
+
+    public GeoJSONGeometry() {
+    }
+
+    /**
+     * POINT
+     */
+    public static class GeoJSONPoint extends GeoJSONGeometry {
+
+        private double[] coordinates = null;
+
+        public GeoJSONPoint() {
+            setType(GeoJSONTypes.POINT);
+        }
+
+        public double[] getCoordinates() {
+            return coordinates;
+        }
+
+        public void setCoordinates(double[] coordinates) {
+            this.coordinates = coordinates;
+        }
+
+    }
+
+    /**
+     * MULTI-POINT
+     */
+    public static class GeoJSONMultiPoint extends GeoJSONGeometry {
+
+        private double[][] coordinates = null;
+
+        public GeoJSONMultiPoint() {
+            setType(GeoJSONTypes.MULTI_POINT);
+        }
+
+        public double[][] getCoordinates() {
+            return coordinates;
+        }
+
+        public void setCoordinates(double[][] coordinates) {
+            this.coordinates = coordinates;
+        }
+    }
+
+    /**
+     * LINESTRING
+     */
+    public static class GeoJSONLineString extends GeoJSONGeometry {
+
+        private double[][] coordinates = null;
+
+        public GeoJSONLineString() {
+            setType(GeoJSONTypes.LINESTRING);
+        }
+
+        public double[][] getCoordinates() {
+            return coordinates;
+        }
+
+        public void setCoordinates(double[][] coordinates) {
+            this.coordinates = coordinates;
+        }
+    }
+
+    /**
+     * MULTI-LINESTRING
+     */
+    public static class GeoJSONMultiLineString extends GeoJSONGeometry {
+
+        private double[][][] coordinates = null;
+
+        public GeoJSONMultiLineString() {
+            setType(GeoJSONTypes.MULTI_LINESTRING);
+        }
+
+        public double[][][] getCoordinates() {
+            return coordinates;
+        }
+
+        public void setCoordinates(double[][][] coordinates) {
+            this.coordinates = coordinates;
+        }
+    }
+
+    /**
+     * POLYGON
+     */
+    public static class GeoJSONPolygon extends GeoJSONGeometry {
+
+        private double[][][] coordinates = null;
+
+        public GeoJSONPolygon() {
+            setType(GeoJSONTypes.POLYGON);
+        }
+
+        public double[][][] getCoordinates() {
+            return coordinates;
+        }
+
+        public void setCoordinates(double[][][] coordinates) {
+            this.coordinates = coordinates;
+        }
+    }
+
+    /**
+     * MULTI-POLYGON
+     */
+    public static  class GeoJSONMultiPolygon extends GeoJSONGeometry {
+
+        private double[][][][] coordinates = null;
+
+        public GeoJSONMultiPolygon() {
+            setType(GeoJSONTypes.MULTI_POLYGON);
+        }
+
+        public double[][][][] getCoordinates() {
+            return coordinates;
+        }
+
+        public void setCoordinates(double[][][][] coordinates) {
+            this.coordinates = coordinates;
+        }
+    }
+
+    /**
+     * GEOMETRY-COLLECTION
+     */
+    public static class GeoJSONGeometryCollection extends GeoJSONGeometry {
+
+        protected List<GeoJSONGeometry> geometries = new ArrayList<GeoJSONGeometry>();
+
+        public GeoJSONGeometryCollection() {
+            setType(GeoJSONTypes.GEOMETRY_COLLECTION);
+        }
+
+        public List<GeoJSONGeometry> getGeometries() {
+            return geometries;
+        }
+
+        public void setGeometries(List<GeoJSONGeometry> geometries) {
+            this.geometries = geometries;
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONObject.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONObject.java
new file mode 100644
index 0000000..5658c23
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/binding/GeoJSONObject.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.sis.storage.geojson.binding;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import static org.apache.sis.storage.geojson.binding.GeoJSONGeometry.*;
+
+import java.io.Serializable;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="type")
+@JsonSubTypes({
+        @JsonSubTypes.Type(value=GeoJSONFeatureCollection.class,    name="FeatureCollection"),
+        @JsonSubTypes.Type(value=GeoJSONFeature.class,              name="Feature"),
+        @JsonSubTypes.Type(value=GeoJSONPoint.class,                name="Point"),
+        @JsonSubTypes.Type(value=GeoJSONLineString.class,           name="LineString"),
+        @JsonSubTypes.Type(value=GeoJSONPolygon.class,              name="Polygon"),
+        @JsonSubTypes.Type(value=GeoJSONMultiPoint.class,           name="MultiPoint"),
+        @JsonSubTypes.Type(value=GeoJSONMultiLineString.class,      name="MultiLineString"),
+        @JsonSubTypes.Type(value=GeoJSONMultiPolygon.class,         name="MultiPolygon"),
+        @JsonSubTypes.Type(value=GeoJSONGeometryCollection.class,   name="GeometryCollection")
+})
+public class GeoJSONObject implements Serializable {
+
+    private String type;
+    private double[] bbox = null;
+    private GeoJSONCRS crs = null;
+
+    public GeoJSONObject() {
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public double[] getBbox() {
+        return bbox;
+    }
+
+    public void setBbox(double[] bbox) {
+        this.bbox = bbox;
+    }
+
+    public GeoJSONCRS getCrs() {
+        return crs;
+    }
+
+    public void setCrs(GeoJSONCRS crs) {
+        this.crs = crs;
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/FeatureTypeUtils.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/FeatureTypeUtils.java
new file mode 100644
index 0000000..8f6e50b
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/FeatureTypeUtils.java
@@ -0,0 +1,596 @@
+/*
+ * 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.sis.storage.geojson.utils;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import static java.nio.file.StandardOpenOption.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.logging.Logger;
+import org.apache.sis.feature.builder.AttributeRole;
+import org.apache.sis.feature.builder.AttributeTypeBuilder;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Static;
+import org.apache.sis.util.iso.Names;
+import org.apache.sis.util.iso.SimpleInternationalString;
+import org.apache.sis.util.logging.Logging;
+import org.locationtech.jts.geom.Geometry;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.FeatureAssociationRole;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.util.FactoryException;
+import org.opengis.util.GenericName;
+import org.opengis.util.InternationalString;
+import org.opengis.util.NameFactory;
+
+/**
+ * An utility class to handle read/write of FeatureType into a JSON schema file.
+ *
+ * Theses schema are inspired from <a href="http://json-schema.org/">JSON-Schema</a> specification.
+ * Changes are :
+ *  - introducing of a {@code javatype} that define Java class used.
+ *  - introducing of a {@code nillable} property for nullable attributes
+ *  - introducing of a {@code userdata} Map property that contain previous user data information.
+ *  - introducing of a {@code geometry} property to describe a geometry
+ *  - introducing of a {@code crs} property to describe a geometry crs
+ *
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public final class FeatureTypeUtils extends Static {
+
+    private static final Logger LOGGER = Logging.getLogger("org.apache.sis.storage.geojson.utils");
+//    private static final FilterFactory2 FF = (FilterFactory2) DefaultFactories.forBuildin(FilterFactory.class);
+
+    private static final String TITLE = "title";
+    private static final String TYPE = "type";
+    private static final String JAVA_TYPE = "javatype";
+    private static final String DESCRIPTION = "description";
+    private static final String PROPERTIES = "properties";
+    private static final String PRIMARY_KEY = "primaryKey";
+    private static final String RESTRICTION = "restriction";
+    private static final String REQUIRED = "required";
+    private static final String MIN_ITEMS = "minItems";
+    private static final String MAX_ITEMS = "maxItems";
+    private static final String USER_DATA = "userdata";
+    private static final String GEOMETRY = "geometry";
+    private static final String GEOMETRY_ATT_NAME = "geometryName";
+    private static final String CRS = "crs";
+
+    private static final String OBJECT = "object";
+    private static final String ARRAY = "array";
+    private static final String INTEGER = "integer";
+    private static final String NUMBER = "number";
+    private static final String STRING = "string";
+    private static final String BOOLEAN = "boolean";
+
+    /**
+     * Write a FeatureType in output File.
+     * @param ft
+     * @param output
+     * @throws IOException
+     */
+    public static void writeFeatureType(FeatureType ft, Path output) throws IOException, DataStoreException {
+        ArgumentChecks.ensureNonNull("FeatureType", ft);
+        ArgumentChecks.ensureNonNull("outputFile", output);
+
+        final AttributeType<?> geom = GeoJSONUtils
+                .castOrUnwrap(GeoJSONUtils.getDefaultGeometry(ft))
+                .orElseThrow(() -> new DataStoreException("No default Geometry in given FeatureType : " + ft));
+
+        try (OutputStream outStream = Files.newOutputStream(output, CREATE, WRITE, TRUNCATE_EXISTING);
+             JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(outStream, JsonEncoding.UTF8)) {
+
+            writer.useDefaultPrettyPrinter();
+            //start write feature collection.
+            writer.writeStartObject();
+            writer.writeStringField(TITLE, ft.getName().tip().toString());
+            writer.writeStringField(TYPE, OBJECT);
+            writer.writeStringField(JAVA_TYPE, "FeatureType");
+            if (ft.getDescription() != null) {
+                writer.writeStringField(DESCRIPTION, ft.getDescription().toString());
+            }
+
+            writeGeometryType(geom, writer);
+            writeProperties(ft, writer);
+
+            writer.writeEndObject();
+            writer.flush();
+        }
+    }
+
+    public static void writeFeatureTypes(List<FeatureType> fts, OutputStream output) throws IOException, DataStoreException {
+        ArgumentChecks.ensureNonNull("FeatureType", fts);
+        ArgumentChecks.ensureNonNull("outputStream", output);
+
+        if (fts.isEmpty()) return;
+
+        if (fts.size() > 1) {
+            JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(output, JsonEncoding.UTF8).useDefaultPrettyPrinter();
+            writer.writeStartArray();
+            for (FeatureType ft : fts) {
+                writeFeatureType(ft, output, writer);
+            }
+            writer.writeEndArray();
+            writer.flush();
+            writer.close();
+        } else {
+            writeFeatureType(fts.get(0), output);
+        }
+    }
+    /**
+     * Write a FeatureType in output File.
+     * @param ft
+     * @param output
+     * @throws IOException
+     */
+    public static void writeFeatureType(FeatureType ft, OutputStream output) throws IOException, DataStoreException {
+        JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(output,JsonEncoding.UTF8).useDefaultPrettyPrinter();
+        writeFeatureType(ft, output, writer);
+        writer.flush();
+        writer.close();
+    }
+
+
+    private static void writeFeatureType(FeatureType ft, OutputStream output, JsonGenerator writer) throws IOException, DataStoreException {
+        ArgumentChecks.ensureNonNull("FeatureType", ft);
+        ArgumentChecks.ensureNonNull("outputStream", output);
+
+        if (GeoJSONUtils.getDefaultGeometry(ft) == null) {
+            throw new DataStoreException("No default Geometry in given FeatureType : "+ft);
+        }
+
+        //start write feature collection.
+        writer.writeStartObject();
+        writer.writeStringField(TITLE, ft.getName().tip().toString());
+        writer.writeStringField(TYPE, OBJECT);
+        writer.writeStringField(JAVA_TYPE, "FeatureType");
+        if (ft.getDescription() != null) {
+            writer.writeStringField(DESCRIPTION, ft.getDescription().toString());
+        }
+
+        final Optional<AttributeType<?>> geom = GeoJSONUtils.castOrUnwrap(
+                GeoJSONUtils.getDefaultGeometry(ft)
+        );
+        if (geom.isPresent()) {
+            writeGeometryType(geom.get(), writer);
+        }
+
+        writeProperties(ft, writer);
+
+        writer.writeEndObject();
+    }
+
+    private static void writeProperties(FeatureType ft, JsonGenerator writer) throws IOException {
+        writer.writeObjectFieldStart(PROPERTIES);
+
+        Collection<? extends PropertyType> descriptors = ft.getProperties(true);
+        List<String> required = new ArrayList<>();
+
+        for (PropertyType type : descriptors) {
+            boolean isRequired = false;
+
+            if (type instanceof FeatureAssociationRole) {
+                isRequired = writeComplexType((FeatureAssociationRole) type, ((FeatureAssociationRole)type).getValueType(), writer);
+            } else if (type instanceof AttributeType) {
+                if(Geometry.class.isAssignableFrom( ((AttributeType) type).getValueClass())){
+//                    GeometryType geometryType = (GeometryType) type;
+//                    isRequired = writeGeometryType(descriptor, geometryType, writer);
+                } else {
+                    isRequired = writeAttributeType(ft, (AttributeType)type, writer);
+                }
+            }
+            if (isRequired) {
+                required.add(type.getName().tip().toString());
+            }
+        }
+
+        if (!required.isEmpty()) {
+            writer.writeArrayFieldStart(REQUIRED);
+            for (String req : required) {
+                writer.writeString(req);
+            }
+            writer.writeEndArray();
+        }
+        writer.writeEndObject();
+    }
+
+    private static boolean writeComplexType(FeatureAssociationRole descriptor, FeatureType complex, JsonGenerator writer)
+            throws IOException {
+
+        writer.writeObjectFieldStart(descriptor.getName().tip().toString());
+        writer.writeStringField(TYPE, OBJECT);
+        writer.writeStringField(JAVA_TYPE, "ComplexType");
+        if (complex.getDescription() != null) {
+            writer.writeStringField(DESCRIPTION, complex.getDescription().toString());
+        }
+        writer.writeNumberField(MIN_ITEMS, descriptor.getMinimumOccurs());
+        writer.writeNumberField(MAX_ITEMS, descriptor.getMaximumOccurs());
+        writeProperties(complex, writer);
+
+        writer.writeEndObject();
+
+        return descriptor.getMinimumOccurs() > 0;
+    }
+
+    private static boolean writeAttributeType(FeatureType featureType, AttributeType att, JsonGenerator writer)
+            throws IOException {
+
+        writer.writeObjectFieldStart(att.getName().tip().toString());
+        Class binding = att.getValueClass();
+
+        writer.writeStringField(TYPE, findType(binding));
+        writer.writeStringField(JAVA_TYPE, binding.getName());
+        if (att.getDescription() != null) {
+            writer.writeStringField(DESCRIPTION, att.getDescription().toString());
+        }
+        if (GeoJSONUtils.isPartOfPrimaryKey(featureType,att.getName().toString())) {
+            writer.writeBooleanField(PRIMARY_KEY, true);
+        }
+        writer.writeNumberField(MIN_ITEMS, att.getMinimumOccurs());
+        writer.writeNumberField(MAX_ITEMS, att.getMaximumOccurs());
+//        List<Filter> restrictions = att.getRestrictions();
+//        if (restrictions != null && !restrictions.isEmpty()) {
+//            final Filter merged = FF.and(restrictions);
+//            writer.writeStringField(RESTRICTION, CQL.write(merged));
+//        }
+        writer.writeEndObject();
+
+        return att.getMinimumOccurs() > 0;
+    }
+
+    private static String findType(Class binding) {
+
+        if (Integer.class.isAssignableFrom(binding)) {
+            return INTEGER;
+        } else if (Number.class.isAssignableFrom(binding)) {
+            return NUMBER;
+        } else if(Boolean.class.isAssignableFrom(binding)) {
+            return BOOLEAN;
+        } else if (binding.isArray()) {
+            return ARRAY;
+        } else  {
+            //fallback
+            return STRING;
+        }
+    }
+
+    private static boolean writeGeometryType(AttributeType geometryType, JsonGenerator writer)
+            throws IOException {
+        writer.writeObjectFieldStart(GEOMETRY);
+        writer.writeStringField(TYPE, OBJECT);
+        if (geometryType.getDescription() != null) {
+            writer.writeStringField(DESCRIPTION, geometryType.getDescription().toString());
+        }
+        writer.writeStringField(JAVA_TYPE, geometryType.getValueClass().getCanonicalName());
+        CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(geometryType);
+        if (crs != null) {
+            final Optional<String> urn = GeoJSONUtils.toURN(crs);
+            if (urn.isPresent()) {
+                writer.writeStringField(CRS, urn.get());
+            }
+        }
+        writer.writeStringField(GEOMETRY_ATT_NAME, geometryType.getName().tip().toString());
+        writer.writeEndObject();
+        return true;
+    }
+
+    /**
+     * Read a FeatureType from an input File.
+     * @param input file to read
+     * @return FeatureType
+     * @throws IOException
+     */
+    public static FeatureType readFeatureType(Path input) throws IOException, DataStoreException {
+
+        try (InputStream stream = Files.newInputStream(input);
+             JsonParser parser = GeoJSONParser.FACTORY.createParser(stream)) {
+
+            final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+            parser.nextToken();
+
+            while (parser.nextToken() != JsonToken.END_OBJECT) {
+
+                final String currName = parser.getCurrentName();
+                switch (currName) {
+                    case TITLE:
+                        ftb.setName(parser.nextTextValue());
+                        break;
+                    case JAVA_TYPE:
+                        String type = parser.nextTextValue();
+                        if (!"FeatureType".equals(type)) {
+                            throw new DataStoreException("Invalid JSON schema : " + input.getFileName().toString());
+                        }
+                        break;
+                    case PROPERTIES:
+                        readProperties(ftb, parser);
+                        break;
+                    case GEOMETRY:
+                        readGeometry(ftb, parser);
+                        break;
+                    case DESCRIPTION:
+                        ftb.setDescription(parser.nextTextValue());
+                        break;
+                }
+            }
+
+            try{
+                return ftb.build();
+            }catch(IllegalStateException ex){
+                throw new DataStoreException("FeatureType name or default geometry not found in JSON schema\n"+ex.getMessage(),ex);
+            }
+        }
+    }
+
+    private static void readGeometry(FeatureTypeBuilder ftb, JsonParser parser)
+            throws IOException, DataStoreException {
+
+        Class<?> binding = null;
+        CoordinateReferenceSystem crs = null;
+        InternationalString description = null;
+        String geometryName = null;
+
+        parser.nextToken(); // {
+        while (parser.nextToken() != JsonToken.END_OBJECT) { // -> }
+            final String currName = parser.getCurrentName();
+            switch (currName) {
+                case JAVA_TYPE:
+                    String javaTypeValue = parser.nextTextValue();
+                    if (!"ComplexType".equals(javaTypeValue)) {
+                        try {
+                            binding = Class.forName(javaTypeValue);
+                        } catch (ClassNotFoundException e) {
+                            throw new DataStoreException("Geometry javatype " + javaTypeValue + " invalid : " + e.getMessage(), e);
+                        }
+                    }
+                    break;
+
+                case CRS:
+                    String crsCode = parser.nextTextValue();
+                    try {
+                        crs = org.apache.sis.referencing.CRS.forCode(crsCode);
+                    } catch (FactoryException e) {
+                        throw new DataStoreException("Geometry crs " + crsCode + " invalid : " + e.getMessage(), e);
+                    }
+                    break;
+
+                case DESCRIPTION:
+                    description = new SimpleInternationalString(parser.nextTextValue());
+                    break;
+                case GEOMETRY_ATT_NAME:
+                    geometryName = parser.nextTextValue();
+            }
+        }
+
+        if (binding == null) {
+            throw new DataStoreException("Binding class not found.");
+        }
+
+        final GenericName name = geometryName != null ? Names.createLocalName(null, null, geometryName) : Names.createLocalName(null, null, "geometry");
+
+        final AttributeTypeBuilder<?> atb = ftb.addAttribute(binding);
+        atb.setName(name);
+        atb.setDescription(description);
+        if (crs != null) {
+            atb.setCRS(crs);
+        }
+        atb.setMinimumOccurs(1);
+        atb.setMaximumOccurs(1);
+        atb.addRole(AttributeRole.DEFAULT_GEOMETRY);
+    }
+
+    private static void readProperties(FeatureTypeBuilder ftb, JsonParser parser)
+            throws IOException, DataStoreException {
+        parser.nextToken(); // {
+
+        List<String> requiredList = null;
+        while (parser.nextToken() != JsonToken.END_OBJECT) { // -> }
+            final JsonToken currToken = parser.getCurrentToken();
+
+            if (currToken == JsonToken.FIELD_NAME) {
+                final String currName = parser.getCurrentName();
+
+                if (REQUIRED.equals(currName)) {
+                    requiredList = parseRequiredArray(parser);
+                } else {
+                    parseProperty(ftb, parser);
+                }
+            }
+        }
+    }
+
+    private static void parseProperty(FeatureTypeBuilder ftb, JsonParser parser)
+            throws IOException, DataStoreException {
+
+        final String attributeName = parser.getCurrentName();
+        Class<?> binding = String.class;
+        boolean primaryKey = false;
+        int minOccurs = 0;
+        int maxOccurs = 1;
+        CharSequence description = null;
+        String restrictionCQL = null;
+        Map<Object, Object> userData = null;
+        FeatureTypeBuilder subftb = null;
+
+        parser.nextToken();
+        while (parser.nextToken() != JsonToken.END_OBJECT) {
+
+            final String currName = parser.getCurrentName();
+            switch (currName) {
+                case JAVA_TYPE:
+                    String javaTypeValue = parser.nextTextValue();
+                    if (!"ComplexType".equals(javaTypeValue)) {
+                        try {
+                            binding = Class.forName(javaTypeValue);
+                        } catch (ClassNotFoundException e) {
+                            throw new DataStoreException("Attribute " + attributeName + " invalid : " + e.getMessage(), e);
+                        }
+                    }
+                    break;
+                case MIN_ITEMS:
+                    minOccurs = parser.nextIntValue(0);
+                    break;
+                case MAX_ITEMS:
+                    maxOccurs = parser.nextIntValue(1);
+                    break;
+                case PRIMARY_KEY:
+                    primaryKey = parser.nextBooleanValue();
+                    break;
+                case RESTRICTION:
+                    restrictionCQL = parser.nextTextValue();
+                    break;
+                case USER_DATA:
+                    userData = parseUserDataMap(parser);
+                    break;
+                case PROPERTIES:
+                    subftb = new FeatureTypeBuilder();
+                    readProperties(subftb, parser);
+                    break;
+                case DESCRIPTION:
+                    description = parser.nextTextValue();
+                    break;
+            }
+        }
+
+        GenericName name = nameValueOf(attributeName);
+        if (subftb == null) {
+            //build AttributeDescriptor
+            if (binding == null) {
+                throw new DataStoreException("Empty javatype for attribute "+attributeName);
+            }
+
+            AttributeTypeBuilder<?> atb = ftb.addAttribute(binding)
+                    .setName(name)
+                    .setDescription(description)
+                    .setMinimumOccurs(minOccurs)
+                    .setMaximumOccurs(maxOccurs);
+
+            if (primaryKey) {
+                atb.addRole(AttributeRole.IDENTIFIER_COMPONENT);
+            }
+
+        } else {
+            //build ComplexType
+            subftb.setName(name);
+            subftb.setDescription(description);
+            final FeatureType complexType = subftb.build();
+
+            ftb.addAssociation(complexType)
+                    .setName(name)
+                    .setMinimumOccurs(minOccurs)
+                    .setMaximumOccurs(maxOccurs);
+        }
+    }
+
+    private static Map<Object, Object> parseUserDataMap(JsonParser parser) throws IOException {
+
+        Map<Object, Object> map = new HashMap<>();
+        parser.nextToken(); // {
+        while (parser.nextToken() != JsonToken.END_OBJECT) {
+            Object key = parser.getCurrentName();
+            JsonToken next = parser.nextToken();
+            map.put(key, GeoJSONParser.getValue(next, parser));
+        }
+        return map;
+
+    }
+
+    private static List<String> parseRequiredArray(JsonParser parser) throws IOException {
+        List<String> requiredList = new ArrayList<>();
+        parser.nextToken(); // [
+
+        while (parser.nextToken() != JsonToken.END_ARRAY) { // -> ]
+            requiredList.add(parser.getValueAsString());
+        }
+
+        return requiredList;
+    }
+
+
+    /**
+     * Parse a string value that can be expressed in 2 different forms :
+     * JSR-283 extended form : {uri}localpart
+     * Separator form : uri:localpart
+     *
+     * if the given string do not match any, then a Name with no namespace will be
+     * created and the localpart will be the given string.
+     */
+    public static GenericName nameValueOf(final String candidate){
+
+        if(candidate.startsWith("{")){
+            //name is in extended form
+            return toSessionNamespaceFromExtended(candidate);
+        }
+
+        int index = candidate.lastIndexOf(':');
+
+        if(index <= 0){
+            return createName(null, candidate);
+        }else{
+            final String uri = candidate.substring(0,index);
+            final String name = candidate.substring(index+1,candidate.length());
+            return createName(uri, name);
+        }
+
+    }
+
+    private static GenericName toSessionNamespaceFromExtended(final String candidate) {
+        final int index = candidate.indexOf('}');
+
+        if(index == -1) throw new IllegalArgumentException("Invalide extended form : "+ candidate);
+
+        final String uri = candidate.substring(1, index);
+        final String name = candidate.substring(index+1, candidate.length());
+
+        return createName(uri, name);
+    }
+
+    /**
+     *
+     * @param namespace if null or empty will not be used for the name
+     * @param local mandatory
+     */
+    public static GenericName createName(final String namespace, final String local) {
+
+        // WARNING: DefaultFactories.NAMES is not a public API and may change in any future SIS version.
+        if(namespace==null || namespace.isEmpty()){
+            return DefaultFactories.forBuildin(NameFactory.class).createGenericName(null, local);
+        }else{
+            return DefaultFactories.forBuildin(NameFactory.class).createGenericName(null, namespace, local);
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONFeatureIterator.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONFeatureIterator.java
new file mode 100644
index 0000000..697b0f4
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONFeatureIterator.java
@@ -0,0 +1,44 @@
+/*
+ * 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.sis.storage.geojson.utils;
+
+import java.io.Closeable;
+import java.util.Iterator;
+import org.apache.sis.storage.geojson.binding.GeoJSONFeature;
+import org.apache.sis.util.collection.BackingStoreException;
+
+/**
+ * Custom FeatureIterator used for lazy parsing of GeoJSONFeature in a json file.
+ *
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public interface GeoJSONFeatureIterator<F extends GeoJSONFeature> extends Iterator<F>, Closeable {
+
+    @Override
+    F next() throws BackingStoreException;
+
+    @Override
+    boolean hasNext() throws BackingStoreException;
+
+    @Override
+    void close() throws BackingStoreException;
+
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONMembres.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONMembres.java
new file mode 100644
index 0000000..9416ba6
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONMembres.java
@@ -0,0 +1,42 @@
+/*
+ * 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.sis.storage.geojson.utils;
+
+import org.apache.sis.util.Static;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public final class GeoJSONMembres extends Static  {
+
+    public static final String TYPE = "type";
+    public static final String FEATURES = "features";
+    public static final String GEOMETRY = "geometry";
+    public static final String GEOMETRIES = "geometries";
+    public static final String COORDINATES = "coordinates";
+    public static final String PROPERTIES = "properties";
+    public static final String CRS = "crs";
+    public static final String NAME = "name";
+    public static final String HREF = "href";
+    public static final String BBOX = "bbox";
+    public static final String ID = "id";
+
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONParser.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONParser.java
new file mode 100644
index 0000000..e7a678f
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONParser.java
@@ -0,0 +1,680 @@
+/*
+ * 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.sis.storage.geojson.utils;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Array;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.storage.geojson.binding.*;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONGeometryCollection;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONLineString;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiLineString;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiPoint;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiPolygon;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONPoint;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONPolygon;
+import static org.apache.sis.storage.geojson.utils.GeoJSONMembres.*;
+import static org.apache.sis.storage.geojson.utils.GeoJSONTypes.*;
+
+/**
+ * Efficient GeoJSONParsing using jackson {@link JsonParser}
+ *
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public final class GeoJSONParser {
+
+    public static final JsonFactory FACTORY = new JsonFactory();
+    public final static Logger LOGGER = Logging.getLogger("org.apache.sis.storage.geojson.utils");
+
+    private GeoJSONParser() {}
+
+    /**
+     * Parse a json file and return a GeoJSONObject.
+     * If parser was construct with lazyParsing as {@code true} and root object
+     * is a FeatureCollection, returned GeoJSONFeatureCollection will only have
+     * start and end feature array location.
+     * Otherwise, all Feature will be parsed and add to GeoJSONFeatureCollection.
+     *
+     * @param jsonFile file to parse
+     * @return GeoJSONObject
+     * @throws IOException
+     */
+    public static GeoJSONObject parse(Path jsonFile) throws IOException {
+        return parse(jsonFile, Boolean.FALSE);
+    }
+
+    /**
+     * Parse a json file and return a GeoJSONObject.
+     * If parser was construct with lazyParsing as {@code true} and root object
+     * is a FeatureCollection, returned GeoJSONFeatureCollection will only have
+     * start and end feature array location.
+     * Otherwise, all Feature will be parsed and add to GeoJSONFeatureCollection.
+     *
+     * @param jsonFile file to parse
+     * @param lazy lazy mode flag
+     * @return GeoJSONObject
+     * @throws IOException
+     */
+    public static GeoJSONObject parse(Path jsonFile, boolean lazy) throws IOException {
+
+        try (InputStream reader = Files.newInputStream(jsonFile);
+             JsonParser p = FACTORY.createParser(reader)) {
+
+            JsonToken startToken = p.nextToken();
+            assert (startToken == JsonToken.START_OBJECT) : "Input File is not a JSON file " + jsonFile.toAbsolutePath().toString();
+            return parseGeoJSONObject(p, lazy, jsonFile);
+        }
+    }
+
+    /**
+     * Parse a json InputStream and return a GeoJSONObject.
+     * In InputStream case, lazy loading of FeatureCollection is
+     * disabled.
+     *
+     * @param inputStream stream to parse
+     * @return GeoJSONObject
+     * @throws IOException
+     */
+    public static GeoJSONObject parse(InputStream inputStream) throws IOException {
+        try (JsonParser p = FACTORY.createParser(inputStream)) {
+            JsonToken startToken = p.nextToken();
+            assert (startToken == JsonToken.START_OBJECT) : "Input stream is not a valid JSON ";
+            return parseGeoJSONObject(p);
+        }
+    }
+
+    /**
+     * Parse a GeoJSONObject (FeatureCollection, Feature or a Geometry)
+     * JsonParser location MUST be on a START_OBJECT token.
+     * @param p parser jackson parser with current token on a START_OBJECT.
+     * @return GeoJSONObject (FeatureCollection, Feature or a Geometry)
+     * @throws IOException
+     */
+
+    public static GeoJSONObject parseGeoJSONObject(JsonParser p) throws IOException {
+        return parseGeoJSONObject(p, Boolean.FALSE, null);
+    }
+
+    /**
+     * Parse a GeoJSONObject (FeatureCollection, Feature or a Geometry)
+     * JsonParser location MUST be on a START_OBJECT token.
+     * @param p parser jackson parser with current token on a START_OBJECT.
+     * @param lazy lazy mode flag
+     * @return GeoJSONObject (FeatureCollection, Feature or a Geometry)
+     * @throws IOException
+     */
+    private static GeoJSONObject parseGeoJSONObject(JsonParser p, Boolean lazy, Path source) throws IOException {
+        assert(p.getCurrentToken() == JsonToken.START_OBJECT);
+
+        GeoJSONObject object = new GeoJSONObject();
+        while (p.nextToken() != JsonToken.END_OBJECT) {
+            String fieldname = p.getCurrentName();
+
+            if (fieldname == null) {
+                throw new IOException("Parsing error, expect object field name value but got null");
+            }
+
+            switch (fieldname) {
+                case ID:
+                    p.nextToken();
+                    String id = p.getValueAsString();
+                    if(object instanceof GeoJSONFeature){
+                        ((GeoJSONFeature)object).setId(id);
+                    }
+                    break;
+                case TYPE:
+                    p.nextToken();
+                    String value = p.getValueAsString();
+                    object = getOrCreateFromType(object, value, lazy);
+                    break;
+
+                case BBOX:
+                    //array
+                    p.nextToken(); // "["
+                    object.setBbox(parseBBoxArray(p));
+                    break;
+
+                case CRS:
+                    //object
+                    p.nextToken(); // "{"
+                    object.setCrs(parseCRS(p));
+                    break;
+
+                case FEATURES:
+                    object = getOrCreateFromType(object, FEATURE_COLLECTION, lazy);
+
+                    //array of GeoJSONFeature
+                    if (Boolean.TRUE.equals(lazy)) {
+                        lazyParseFeatureCollection((GeoJSONFeatureCollection) object, p, source);
+                    } else {
+                        parseFeatureCollection((GeoJSONFeatureCollection) object, p);
+                    }
+                    break;
+
+                case PROPERTIES:
+                    object = getOrCreateFromType(object, FEATURE);
+                    //object
+                    parseProperties((GeoJSONFeature) object, p);
+                    break;
+
+                case GEOMETRY:
+                    object = getOrCreateFromType(object, FEATURE);
+                    //object
+                    parseFeatureGeometry((GeoJSONFeature) object, p);
+                    break;
+
+                case COORDINATES:
+                    //array
+                    if (object instanceof GeoJSONGeometry) {
+                        parseGeometry((GeoJSONGeometry) object, p);
+                    } else {
+                        LOGGER.log(Level.WARNING, "Error need type before coordinates");
+                    }
+                    break;
+
+                case GEOMETRIES:
+                    if (object instanceof GeoJSONGeometryCollection) {
+                        object = getOrCreateFromType(object, GEOMETRY_COLLECTION);
+                        //array of GeoJSONGeometry
+                        parseGeometryCollection((GeoJSONGeometryCollection) object, p);
+                    } else {
+                        LOGGER.log(Level.WARNING, "Error need type before coordinates");
+                    }
+                    break;
+                default :
+                    if(p.getCurrentToken()==JsonToken.START_OBJECT){
+                        //skip any unknown properties
+                        parseGeoJSONObject(p,lazy,source);
+                    }
+                    break;
+            }
+        }
+        return object;
+    }
+
+    /**
+     * Parse properties map and add to given Feature
+     * @param feature Feature to attach properties
+     * @param p parser
+     * @throws IOException
+     */
+    private static void parseProperties(GeoJSONFeature feature, JsonParser p) throws IOException {
+        p.nextToken(); // "{"
+        feature.getProperties().putAll(parseMap(p));
+    }
+
+    /**
+     * Parse a map of String, Object.
+     * JsonParser location MUST be on a START_OBJECT token.
+     * @param p parser jackson parser with current token on a START_OBJECT.
+     * @return Map of String - Object
+     * @throws IOException
+     */
+    private static Map<String, Object> parseMap(JsonParser p) throws IOException {
+        Map<String, Object> map = new HashMap<>();
+        final JsonToken currentToken = p.getCurrentToken();
+        if (currentToken == JsonToken.VALUE_NULL) {
+            return map;
+        }
+
+        if (currentToken != JsonToken.START_OBJECT) {
+            LOGGER.log(Level.WARNING, "Expect START_OBJECT token but got "+currentToken+" for "+p.getCurrentName());
+            return map;
+        }
+
+        assert(currentToken == JsonToken.START_OBJECT);
+        while (p.nextToken() != JsonToken.END_OBJECT) {
+            String key = p.getCurrentName();
+            JsonToken next = p.nextToken();
+
+            map.put(key, getValue(next, p));
+        }
+        return map;
+    }
+
+    /**
+     * Parse a List of Objects.
+     * JsonParser location MUST be on a START_ARRAY token.
+     * @param p parser jackson parser with current token on a START_ARRAY.
+     * @return List of Objects
+     * @throws IOException
+     */
+    private static List<Object> parseArray(JsonParser p) throws IOException {
+        assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+        List<Object> list = new ArrayList<>();
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            list.add(getValue(p.getCurrentToken(), p));
+        }
+        return list;
+    }
+
+    /**
+     * Parse a List of Objects.
+     * JsonParser location MUST be on a START_ARRAY token.
+     * @param p parser jackson parser with current token on a START_ARRAY.
+     * @return array object typed after first element class
+     * @throws IOException
+     */
+    private static Object parseArray2(JsonParser p) throws IOException {
+        assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+        List<Object> list = new ArrayList<>();
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            list.add(getValue(p.getCurrentToken(), p));
+        }
+
+        if (list.isEmpty()) {
+            return new Object[0];
+        }
+
+        Class binding = list.get(0).getClass();
+        Object newArray = Array.newInstance(binding, list.size());
+        for (int i = 0; i < list.size(); i++) {
+            Array.set(newArray, i, list.get(i));
+        }
+
+        return newArray;
+    }
+
+    /**
+     * Convert the current token to appropriate object.
+     * Supported (String, Integer, Float, Boolean, Null, Array, Map)
+     *
+     * @param token current token
+     * @param p parser
+     * @return current token value String or Integer or Float or Boolean or null or an array or a map.
+     * @throws IOException
+     */
+    static Object getValue(JsonToken token, JsonParser p) throws IOException {
+        if (token == JsonToken.VALUE_STRING) {
+            return p.getValueAsString();
+        } else if (token == JsonToken.VALUE_NUMBER_INT) {
+            final long value = p.getValueAsLong();
+            if(value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE) {
+                return (int) value;
+            }
+            return value;
+        } else if (token == JsonToken.VALUE_NUMBER_FLOAT) {
+            return p.getValueAsDouble();
+        } else if (token == JsonToken.VALUE_TRUE || token == JsonToken.VALUE_FALSE) {
+                return token == JsonToken.VALUE_TRUE;
+        } else if (token == JsonToken.VALUE_NULL) {
+            return null;
+        } else if (token == JsonToken.START_ARRAY) {
+            return parseArray2(p);
+        } else if (token == JsonToken.START_OBJECT) {
+            return parseMap(p);
+        } else {
+            throw new UnsupportedOperationException("Unsupported JSON token : "+token+ ", value : "+p.getText());
+        }
+    }
+
+    /**
+     * Parse a coordinates array(s) and add to given GeoJSONGeometry.
+     * @param geom GeoJSONGeometry
+     * @param p parser
+     * @throws IOException
+     */
+    private static void parseGeometry(GeoJSONGeometry geom, JsonParser p) throws IOException {
+        p.nextToken(); // "["
+        if (geom instanceof GeoJSONPoint) {
+            ((GeoJSONPoint) geom).setCoordinates(parsePoint(p));
+        } else if (geom instanceof GeoJSONLineString) {
+            ((GeoJSONLineString) geom).setCoordinates(parseLineString(p));
+        } else if (geom instanceof GeoJSONPolygon) {
+            ((GeoJSONPolygon) geom).setCoordinates(parseMultiLineString(p));
+        } else if (geom instanceof GeoJSONMultiPoint) {
+            ((GeoJSONMultiPoint) geom).setCoordinates(parseLineString(p));
+        } else if (geom instanceof GeoJSONMultiLineString) {
+            ((GeoJSONMultiLineString) geom).setCoordinates(parseMultiLineString(p));
+        } else if (geom instanceof GeoJSONMultiPolygon) {
+            ((GeoJSONMultiPolygon) geom).setCoordinates(parseMultiPolygon(p));
+        }
+    }
+
+    /**
+     * Full parse of GeoJSONFeatures for a GeoJSONFeatureCollection
+     * @param coll GeoJSONFeatureCollection
+     * @param p parser
+     * @throws IOException
+     */
+    private static void parseFeatureCollection(GeoJSONFeatureCollection coll, JsonParser p) throws IOException {
+        p.nextToken(); // "{"
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+
+            GeoJSONObject obj = parseGeoJSONObject(p, false, null);
+            if (obj instanceof GeoJSONFeature) {
+                coll.getFeatures().add((GeoJSONFeature)obj);
+            } else {
+                LOGGER.log(Level.WARNING, "ERROR feature collection");
+            }
+        }
+    }
+
+    /**
+     * Lazy parse of GeoJSONFeatures for a GeoJSONFeatureCollection.
+     * Only find an set START_ARRAY and END_ARRAY TokenLocation of the features array to
+     * the GeoJSONFeatureCollection object.
+     *
+     * @param coll GeoJSONFeatureCollection
+     * @param p parser
+     * @param source
+     * @throws IOException
+     */
+    private static void lazyParseFeatureCollection(GeoJSONFeatureCollection coll, JsonParser p, Path source) throws IOException {
+
+        p.nextToken();
+        coll.setSourceInput(source);
+        coll.setStartPosition(p.getCurrentLocation());
+
+        int startArray = 1;
+        int endArray = 0;
+
+        //loop to the right "]"
+        while (startArray != endArray) {
+            JsonToken token = p.nextToken();
+            if (token ==  JsonToken.START_ARRAY) startArray ++;
+            if (token ==  JsonToken.END_ARRAY) endArray ++;
+        }
+
+        coll.setEndPosition(p.getCurrentLocation());
+    }
+
+    /**
+     * Parse GeoJSONGeometry for GeoJSONFeature.
+     * @param feature GeoJSONFeature
+     * @param p parser
+     * @throws IOException
+     */
+    private static void parseFeatureGeometry(GeoJSONFeature feature, JsonParser p) throws IOException {
+        p.nextToken(); // "{"
+        GeoJSONObject obj = parseGeoJSONObject(p);
+        assert(obj != null) : "Un-parsable GeoJSONGeometry.";
+        assert(obj instanceof GeoJSONGeometry) : "Unexpected GeoJSONObject : " + obj.getType()+" expected : GeoJSONGeometry";
+        feature.setGeometry((GeoJSONGeometry) obj);
+    }
+
+    /**
+     * Parse GeoJSONGeometry for GeoJSONGeometryCollection.
+     * @param geom GeoJSONGeometryCollection
+     * @param p parser
+     * @throws IOException
+     */
+    private static void parseGeometryCollection(GeoJSONGeometryCollection geom, JsonParser p) throws IOException {
+        p.nextToken(); // "["
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            GeoJSONObject obj = parseGeoJSONObject(p);
+            assert(obj != null) : "Un-parsable GeoJSONGeometry.";
+            assert(obj instanceof GeoJSONGeometry) : "Unexpected GeoJSONObject : " + obj.getType()+" expected : GeoJSONGeometry";
+            geom.getGeometries().add((GeoJSONGeometry)obj);
+        }
+    }
+
+    /**
+     * Create GeoJSONObject using type.
+     * If previous object is not null, forward bbox and crs parameter to the new GeoJSONObject.
+     *
+     * @param object previous object.
+     * @param type see {@link org.apache.sis.storage.geojson.utils.GeoJSONTypes}
+     * @return GeoJSONObject
+     */
+    private static GeoJSONObject getOrCreateFromType(GeoJSONObject object, String type) {
+        return getOrCreateFromType(object, type, Boolean.FALSE);
+
+    }
+
+    /**
+     * Create GeoJSONObject using type.
+     * If previous object is not null, forward bbox and crs parameter to the new GeoJSONObject.
+     *
+     * @param object previous object.
+     * @param type see {@link org.apache.sis.storage.geojson.utils.GeoJSONTypes}
+     * @param lazy lazy mode flag
+     * @return GeoJSONObject
+     */
+    private static GeoJSONObject getOrCreateFromType(GeoJSONObject object, String type, Boolean lazy) {
+
+        GeoJSONObject result = object;
+        if (type != null) {
+            switch (type) {
+                case FEATURE_COLLECTION:
+                    if (result instanceof GeoJSONFeatureCollection) {
+                        return result;
+                    } else {
+                        result = new GeoJSONFeatureCollection(lazy);
+                    }
+                    break;
+                case FEATURE:
+                    if (result instanceof GeoJSONFeature) {
+                        return result;
+                    } else {
+                        result = new GeoJSONFeature();
+                    }
+                    break;
+                case POINT:
+                    if (result instanceof GeoJSONPoint) {
+                        return result;
+                    } else {
+                        result = new GeoJSONPoint();
+                    }
+                    break;
+                case LINESTRING:
+                    if (result instanceof GeoJSONLineString) {
+                        return result;
+                    } else {
+                        result = new GeoJSONLineString();
+                    }
+                    break;
+                case POLYGON:
+                    if (result instanceof GeoJSONPolygon) {
+                        return result;
+                    } else {
+                        result = new GeoJSONPolygon();
+                    }
+                    break;
+                case MULTI_POINT:
+                    if (result instanceof GeoJSONMultiPoint) {
+                        return result;
+                    } else {
+                        result = new GeoJSONMultiPoint();
+                    }
+                    break;
+                case MULTI_LINESTRING:
+                    if (result instanceof GeoJSONMultiLineString) {
+                        return result;
+                    } else {
+                        result = new GeoJSONMultiLineString();
+                    }
+                    break;
+                case MULTI_POLYGON:
+                    if (result instanceof GeoJSONMultiPolygon) {
+                        return result;
+                    } else {
+                        result = new GeoJSONMultiPolygon();
+                    }
+                    break;
+                case GEOMETRY_COLLECTION:
+                    if (result instanceof GeoJSONGeometryCollection) {
+                        return result;
+                    } else {
+                        result = new GeoJSONGeometryCollection();
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException("Unknown type " + type);
+            }
+
+            if (object != null) {
+                result.setBbox(object.getBbox());
+                result.setCrs(object.getCrs());
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Parse the bbox array.
+     * JsonParser location MUST be on a START_ARRAY token.
+     * @param p JsonParser location MUST be on a START_ARRAY token.
+     * @return  an array of double with a length of 4 or 6.
+     * @throws IOException
+     */
+    private static double[] parseBBoxArray(JsonParser p) throws IOException {
+        assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+        double[] bbox = new double[4];
+
+        int idx = 0;
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            if (idx == 4) {
+                bbox = Arrays.copyOf(bbox, 6);
+            }
+            bbox[idx++] = p.getDoubleValue();
+        }
+
+        return bbox;
+    }
+
+    /**
+     * Parse a CRS Object.
+     * JsonParser location MUST be on a START_OBJECT token.
+     * @param p JsonParser location MUST be on a START_OBJECT token.
+     * @return GeoJSONCRS
+     * @throws IOException
+     */
+    private static GeoJSONCRS parseCRS(JsonParser p) throws IOException {
+        assert(p.getCurrentToken() == JsonToken.START_OBJECT);
+        GeoJSONCRS crs = new GeoJSONCRS();
+
+        while (p.nextToken() != JsonToken.END_OBJECT) {
+            String fieldName = p.getCurrentName();
+            if (TYPE.equals(fieldName)) {
+                crs.setType(p.nextTextValue());
+            } else if (PROPERTIES.equals(fieldName)) {
+                //object
+                p.nextToken();
+                while (p.nextToken() != JsonToken.END_OBJECT) {
+                    crs.getProperties().put(p.getCurrentName(), p.getValueAsString());
+                }
+            }
+        }
+
+        return crs;
+    }
+
+    /**
+     * Parse a Coordinate.
+     * JsonParser location MUST be on a START_ARRAY token.
+     * @param p JsonParser location MUST be on a START_ARRAY token.
+     * @return an array of double like [X,Y,(Z)]
+     * @throws IOException
+     */
+    private static double[] parsePoint(JsonParser p) throws IOException {
+        assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+        double[] pt = new double[2];
+
+        int idx = 0;
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            if (idx == 2) {
+                pt = Arrays.copyOf(pt, 3);
+            }
+            pt[idx++] = p.getDoubleValue();
+        }
+        return pt;
+    }
+
+    /**
+     * Parse a LineString/MultiPoint.
+     * JsonParser location MUST be on a START_ARRAY token.
+     * @param p JsonParser location MUST be on a START_ARRAY token.
+     * @return an array of double like [[X0,Y0,(Z0)], [X1,Y1,(Z1)]]
+     * @throws IOException
+     */
+    private static double[][] parseLineString(JsonParser p) throws IOException {
+        assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+        List<double[]> line = new ArrayList<>();
+
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            line.add(parsePoint(p));
+        }
+        return line.toArray(new double[line.size()][]);
+    }
+
+    /**
+     * Parse a List of LineString or Polygon.
+     * JsonParser location MUST be on a START_ARRAY token.
+     * @param p JsonParser location MUST be on a START_ARRAY token.
+     * @return an array of double like
+     * [
+     *  [[X0,Y0,(Z0)], [X1,Y1,(Z1)]],
+     *  [[X0,Y0,(Z0)], [X1,Y1,(Z1)]], ...
+     * ]
+     * @throws IOException
+     */
+    private static double[][][] parseMultiLineString(JsonParser p) throws IOException {
+        assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+        List<double[][]> lines = new ArrayList<>();
+
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            lines.add(parseLineString(p));
+        }
+        return lines.toArray(new double[lines.size()][][]);
+    }
+
+    /**
+     * Parse a List of Polygons (list of list of LineString).
+     * JsonParser location MUST be on a START_ARRAY token.
+     * @param p JsonParser location MUST be on a START_ARRAY token.
+     * @return an array of double like
+     * [[
+     *      [[X0,Y0,(Z0)], [X1,Y1,(Z1)]],
+     *      [[X0,Y0,(Z0)], [X1,Y1,(Z1)]]
+     *  ],[
+     *      [[X0,Y0,(Z0)], [X1,Y1,(Z1)]],
+     *      [[X0,Y0,(Z0)], [X1,Y1,(Z1)]]
+     *  ], ... ]
+     * @throws IOException
+     */
+    private static double[][][][] parseMultiPolygon(JsonParser p) throws IOException {
+        assert(p.getCurrentToken() == JsonToken.START_ARRAY);
+        List<double[][][]> polygons = new ArrayList<>();
+
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            polygons.add(parseMultiLineString(p));
+        }
+        return polygons.toArray(new double[polygons.size()][][][]);
+    }
+
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONTypes.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONTypes.java
new file mode 100644
index 0000000..cc580d9
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONTypes.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.sis.storage.geojson.utils;
+
+import org.apache.sis.util.Static;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public final class GeoJSONTypes extends Static {
+
+    public static final String FEATURE_COLLECTION = "FeatureCollection";
+    public static final String FEATURE = "Feature";
+
+    public static final String POINT = "Point";
+    public static final String LINESTRING = "LineString";
+    public static final String POLYGON = "Polygon";
+    public static final String MULTI_POINT = "MultiPoint";
+    public static final String MULTI_LINESTRING = "MultiLineString";
+    public static final String MULTI_POLYGON = "MultiPolygon";
+    public static final String GEOMETRY_COLLECTION = "GeometryCollection";
+
+    public static final String CRS_NAME = "name";
+    public static final String CRS_LINK = "link";
+
+    public static final String CRS_TYPE_PROJ4 = "proj4";
+    public static final String CRS_TYPE_OGCWKT = "ogcwkt";
+    public static final String CRS_TYPE_ESRIWKT = "esriwkt";
+
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONUtils.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONUtils.java
new file mode 100644
index 0000000..55a41ac
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeoJSONUtils.java
@@ -0,0 +1,581 @@
+/*
+ * 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.sis.storage.geojson.utils;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonLocation;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.io.wkt.Convention;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.storage.geojson.binding.GeoJSONObject;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.util.FactoryException;
+
+import java.io.*;
+import java.lang.reflect.Array;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.ParseException;
+import java.util.Collection;
+import java.util.logging.Level;
+
+import org.apache.sis.util.Utilities;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.nio.file.StandardOpenOption.WRITE;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import org.apache.sis.feature.AbstractOperation;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.internal.storage.io.IOUtilities;
+import org.apache.sis.io.wkt.WKTFormat;
+import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.util.Numbers;
+import static org.apache.sis.storage.geojson.utils.GeoJSONMembres.*;
+import static org.apache.sis.storage.geojson.utils.GeoJSONTypes.*;
+import org.apache.sis.util.Static;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.IdentifiedType;
+import org.opengis.feature.Operation;
+import org.opengis.feature.PropertyNotFoundException;
+import org.opengis.feature.PropertyType;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public final class GeoJSONUtils extends Static {
+
+    /**
+     * Fallback CRS
+     */
+    private static final CoordinateReferenceSystem DEFAULT_CRS = CommonCRS.WGS84.normalizedGeographic();
+
+    /**
+     * A test to know if a given property is an SIS convention or not. Return true if
+     * the property is NOT marked as an SIS convention, false otherwise.
+     */
+    public static final Predicate<IdentifiedType> IS_NOT_CONVENTION = p -> !AttributeConvention.contains(p.getName());
+
+    /**
+     * Extract the coordinate reference system associated to the primary geometry
+     * of input data type.
+     *
+     * @implNote
+     * Primary geometry is determined using {@link #getDefaultGeometry(org.opengis.feature.FeatureType) }.
+     *
+     * @param type The data type to extract reference system from.
+     * @return The CRS associated to the default geometry of this data type, or
+     * a null value if we cannot determine what is the primary geometry of the
+     * data type. Note that a null value is also returned if a geometry property
+     * is found, but no CRS characteristics is associated with it.
+     */
+    public static CoordinateReferenceSystem getCRS(FeatureType type){
+        try {
+            return getCRS(getDefaultGeometry(type));
+        } catch (IllegalArgumentException|IllegalStateException ex) {
+            //no default geometry property
+            return null;
+        }
+    }
+
+    /**
+     * Extract CRS characteristic if it exist.
+     *
+     * @param type
+     * @return CoordinateReferenceSystem or null
+     */
+    public static CoordinateReferenceSystem getCRS(PropertyType type){
+        return getCharacteristicValue(type, AttributeConvention.CRS_CHARACTERISTIC.toString(), null);
+    }
+
+    /**
+     * Extract characteristic value if it exist.
+     *
+     * @param <T> expected value class
+     * @param type base type to search in
+     * @param charName characteristic name
+     * @param defaulValue default value if characteristic is missing or null.
+     * @return characteristic value or default value is not found
+     */
+    public static <T> T getCharacteristicValue(PropertyType type, String charName, T defaulValue){
+        while(type instanceof Operation){
+            type = (PropertyType) ((Operation)type).getResult();
+        }
+        if(type instanceof AttributeType){
+            final AttributeType at = (AttributeType) ((AttributeType)type).characteristics().get(charName);
+            if(at!=null){
+                T val = (T) at.getDefaultValue();
+                return val==null ? defaulValue : val;
+            }
+        }
+        return defaulValue;
+    }
+
+    /**
+     * Search for the main geometric property in the given type. We'll search
+     * for an SIS convention first (see
+     * {@link AttributeConvention#GEOMETRY_PROPERTY}. If no convention is set on
+     * the input type, we'll check if it contains a single geometric property.
+     * If it's the case, we return it. Otherwise (no or multiple geometries), we
+     * throw an exception.
+     *
+     * @param type The data type to search into.
+     * @return The main geometric property we've found.
+     * @throws PropertyNotFoundException If no geometric property is available
+     * in the given type.
+     * @throws IllegalStateException If no convention is set (see
+     * {@link AttributeConvention#GEOMETRY_PROPERTY}), and we've found more than
+     * one geometry.
+     */
+    public static PropertyType getDefaultGeometry(final FeatureType type) throws PropertyNotFoundException, IllegalStateException {
+        PropertyType geometry;
+        try {
+            geometry = type.getProperty(AttributeConvention.GEOMETRY_PROPERTY.toString());
+        } catch (PropertyNotFoundException e) {
+            try {
+                geometry = searchForGeometry(type);
+            } catch (RuntimeException e2) {
+                e2.addSuppressed(e);
+                throw e2;
+            }
+        }
+
+        return geometry;
+    }
+
+    /**
+     * Search for a geometric attribute outside SIS conventions. More accurately,
+     * we expect the given type to have a single geometry attribute. If many are
+     * found, an exception is thrown.
+     *
+     * @param type The data type to search into.
+     * @return The only geometric property we've found.
+     * @throws PropertyNotFoundException If no geometric property is available in
+     * the given type.
+     * @throws IllegalStateException If we've found more than one geometry.
+     */
+    private static PropertyType searchForGeometry(final FeatureType type) throws PropertyNotFoundException, IllegalStateException {
+        final List<? extends PropertyType> geometries = type.getProperties(true).stream()
+                .filter(IS_NOT_CONVENTION)
+                .filter(AttributeConvention::isGeometryAttribute)
+                .collect(Collectors.toList());
+
+        if (geometries.size() < 1) {
+            throw new PropertyNotFoundException("No geometric property can be found outside of sis convention.");
+        } else if (geometries.size() > 1) {
+            throw new IllegalStateException("Multiple geometries found. We don't know which one to select.");
+        } else {
+            return geometries.get(0);
+        }
+    }
+
+    /**
+     * Get main geometry property value. The ways this method determines default
+     * geometry property are the same as {@link #getDefaultGeometry(org.opengis.feature.FeatureType) }.
+     *
+     * @param input the feature to extract geometry from.
+     * @return Value of the main geometric property of the given feature. The returned
+     * optional will be empty only if the feature defines a geometric property, but has
+     * no value for it.
+     * @throws PropertyNotFoundException If no geometric property is available in
+     * the given feature.
+     * @throws IllegalStateException If we've found more than one geometry.
+     */
+    public static Optional<Object> getDefaultGeometryValue(Feature input) throws PropertyNotFoundException, IllegalStateException {
+        Object geometry;
+        try{
+            geometry = input.getPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString());
+        } catch(PropertyNotFoundException ex) {
+            try {
+                final PropertyType geomType = getDefaultGeometry(input.getType());
+                geometry = input.getPropertyValue(geomType.getName().toString());
+            } catch (RuntimeException e) {
+                e.addSuppressed(ex);
+                throw e;
+            }
+        }
+
+        return Optional.ofNullable(geometry);
+    }
+
+    /**
+     * Parse LinkedCRS (href + type).
+     * @param href
+     * @param crsType
+     * @return CoordinateReferenceSystem or null.
+     */
+    public static CoordinateReferenceSystem parseCRS(String href, String crsType) {
+        String wkt = null;
+        try(InputStream stream = new URL(href).openStream()) {
+            wkt = IOUtilities.toString(stream);
+        } catch (IOException e) {
+            GeoJSONParser.LOGGER.log(Level.WARNING, "Can't access to linked CRS "+href, e);
+        }
+
+        if (wkt != null) {
+            WKTFormat format = new WKTFormat(Locale.ENGLISH, TimeZone.getTimeZone("GMT"));
+            if (crsType.equals(CRS_TYPE_OGCWKT)) {
+                format.setConvention(Convention.WKT1);
+            } else if (crsType.equals(CRS_TYPE_ESRIWKT)) {
+                format.setConvention(Convention.WKT1_COMMON_UNITS);
+            }
+            try {
+                Object obj = format.parseObject(wkt);
+                if (obj instanceof CoordinateReferenceSystem) {
+                    return (CoordinateReferenceSystem) obj;
+                } else {
+                    GeoJSONParser.LOGGER.log(Level.WARNING, "Parsed WKT is not a CRS "+wkt);
+                }
+            } catch (ParseException e) {
+                GeoJSONParser.LOGGER.log(Level.WARNING, "Can't parse CRS WKT " + crsType+ " : "+wkt, e);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Test if given data type is an attribute as defined by {@link AttributeType},
+     * or if it depends on an attribute, and return it (the attribute) if possible.
+     * @param input the data type to unravel the attribute from.
+     * @return The found attribute or an empty shell if we cannot find any.
+     */
+    public static Optional<AttributeType<?>> castOrUnwrap(IdentifiedType input) {
+        // In case an operation also implements attribute type, we check it first.
+        // TODO : cycle detection ?
+        while (!(input instanceof AttributeType) && input instanceof Operation) {
+            input = ((Operation)input).getResult();
+        }
+
+        if (input instanceof AttributeType) {
+            return Optional.of((AttributeType)input);
+        }
+
+        return Optional.empty();
+    }
+
+    /**
+     * Returns true if property is a component of the feature type primary key.
+     */
+    public static boolean isPartOfPrimaryKey(FeatureType type, String propertyName) {
+        PropertyType property;
+        try{
+            property = type.getProperty(AttributeConvention.IDENTIFIER_PROPERTY.toString());
+        } catch(PropertyNotFoundException ex) {
+            //no identifier property
+            return false;
+        }
+        if(property instanceof AbstractOperation){
+            final Set<String> dependencies = ((AbstractOperation)property).getDependencies();
+            return dependencies.contains(propertyName);
+        }
+        return false;
+    }
+
+    /**
+     * Convert a CoordinateReferenceSystem to a identifier string like
+     * urn:ogc:def:crs:EPSG::4326
+     * @param crs
+     * @return
+     */
+    public static Optional<String> toURN(CoordinateReferenceSystem crs) {
+        ArgumentChecks.ensureNonNull("crs", crs);
+
+        String urn = null;
+        try {
+            if (Utilities.equalsIgnoreMetadata(crs, CommonCRS.WGS84.normalizedGeographic())) {
+                crs = CommonCRS.WGS84.normalizedGeographic();
+            }
+
+            urn = IdentifiedObjects.lookupURN(crs, Citations.EPSG);
+        } catch (FactoryException e) {
+            GeoJSONParser.LOGGER.log(Level.WARNING, "Unable to extract epsg code from given CRS "+crs, e);
+        }
+
+        return Optional.ofNullable(urn);
+    }
+
+    /**
+     * Try to extract/parse the CoordinateReferenceSystem from a GeoJSONObject.
+     * Use WGS_84 as fallback CRS.
+     * @param obj GeoJSONObject
+     * @return GeoJSONObject CoordinateReferenceSystem or fallback CRS (WGS84).
+     * @throws MalformedURLException
+     * @throws DataStoreException
+     */
+    public static CoordinateReferenceSystem getCRS(GeoJSONObject obj) throws MalformedURLException, DataStoreException {
+        CoordinateReferenceSystem crs = null;
+        try {
+            if (obj.getCrs() != null) {
+                crs = obj.getCrs().getCRS();
+            }
+        } catch (FactoryException e) {
+            throw new DataStoreException(e.getMessage(), e);
+        }
+
+        if (crs == null) {
+            crs = DEFAULT_CRS;
+        }
+        return crs;
+    }
+
+    /**
+     * Utility method Create geotk Envelope if bbox array is filled.
+     * @return Envelope or null.
+     */
+    public static Envelope getEnvelope(GeoJSONObject obj, CoordinateReferenceSystem crs) {
+
+        double[] bbox = obj.getBbox();
+        if (bbox != null) {
+            GeneralEnvelope env = new GeneralEnvelope(crs);
+            int dim = bbox.length/2;
+            if (dim == 2) {
+                env.setRange(0, bbox[0], bbox[2]);
+                env.setRange(1, bbox[1], bbox[3]);
+            } else if (dim == 3) {
+                env.setRange(0, bbox[0], bbox[3]);
+                env.setRange(1, bbox[1], bbox[4]);
+            }
+            return env;
+        }
+        return null;
+    }
+
+    /**
+     * Return file name without extension
+     * @param file candidate
+     * @return String
+     */
+    public static String getNameWithoutExt(Path file) {
+        return IOUtilities.filenameWithoutExtension(file.toUri().toString());
+    }
+
+    /**
+     * Returns the filename extension from a {@link String}, {@link File}, {@link URL} or
+     * {@link URI}. If no extension is found, returns an empty string.
+     *
+     * @param  path The path as a {@link String}, {@link File}, {@link URL} or {@link URI}.
+     * @return The filename extension in the given path, or an empty string if none.
+     */
+    public static String extension(final Object path) {
+        return IOUtilities.extension(path);
+    }
+
+    /**
+     * Write an empty FeatureCollection in a file
+     * @param f output file
+     * @throws IOException
+     */
+    public static void writeEmptyFeatureCollection(Path f) throws IOException {
+
+        try (OutputStream outStream = Files.newOutputStream(f, CREATE, WRITE, TRUNCATE_EXISTING);
+             JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(outStream, JsonEncoding.UTF8)) {
+
+            //start write feature collection.
+            writer.writeStartObject();
+            writer.writeStringField(TYPE, FEATURE_COLLECTION);
+            writer.writeArrayFieldStart(FEATURES);
+            writer.writeEndArray();
+            writer.writeEndObject();
+            writer.flush();
+        }
+    }
+
+    /**
+     * Useful method to help write an object into a JsonGenerator.
+     * This method can handle :
+     * <ul>
+     *     <li>Arrays</li>
+     *     <li>Collection</li>
+     *     <li>Numbers (Double, Float, Short, BigInteger, BigDecimal, integer, Long, Byte)</li>
+     *     <li>Boolean</li>
+     *     <li>String</li>
+     * </ul>
+     * @param value
+     * @param writer
+     * @throws IOException
+     * @throws IllegalArgumentException
+     */
+    public static void writeValue(Object value, JsonGenerator writer) throws IOException, IllegalArgumentException {
+
+        if (value == null) {
+            writer.writeNull();
+            return;
+        }
+
+        Class binding = value.getClass();
+
+        if (binding.isArray()) {
+            if (byte.class.isAssignableFrom(binding.getComponentType())) {
+                writer.writeBinary((byte[])value);
+            } else {
+                writer.writeStartArray();
+                final int size = Array.getLength(value);
+                for (int i = 0; i < size; i++) {
+                    writeValue(Array.get(value, i), writer);
+                }
+                writer.writeEndArray();
+            }
+
+        } else if (Collection.class.isAssignableFrom(binding)) {
+            writer.writeStartArray();
+            Collection coll = (Collection) value;
+            for (Object obj : coll) {
+                writeValue(obj, writer);
+            }
+            writer.writeEndArray();
+
+        } else if (Double.class.isAssignableFrom(binding)) {
+            writer.writeNumber((Double) value);
+        } else if (Float.class.isAssignableFrom(binding)) {
+            writer.writeNumber((Float) value);
+        } else if (Short.class.isAssignableFrom(binding)) {
+            writer.writeNumber((Short) value);
+        } else if (Byte.class.isAssignableFrom(binding)) {
+            writer.writeNumber((Byte) value);
+        } else if (BigInteger.class.isAssignableFrom(binding)) {
+            writer.writeNumber((BigInteger) value);
+        } else if (BigDecimal.class.isAssignableFrom(binding)) {
+            writer.writeNumber((BigDecimal) value);
+        } else if (Integer.class.isAssignableFrom(binding)) {
+            writer.writeNumber((Integer) value);
+        } else if (Long.class.isAssignableFrom(binding)) {
+            writer.writeNumber((Long) value);
+
+        } else if (Boolean.class.isAssignableFrom(binding)) {
+            writer.writeBoolean((Boolean) value);
+        } else if (String.class.isAssignableFrom(binding)) {
+            writer.writeString(String.valueOf(value));
+        } else {
+            //fallback
+            writer.writeString(String.valueOf(value));
+        }
+    }
+
+    /**
+     * Compare {@link JsonLocation} equality without sourceRef test.
+     * @param loc1
+     * @param loc2
+     * @return
+     */
+    public static boolean equals(JsonLocation loc1, JsonLocation loc2) {
+        if (loc1 == null) {
+            return (loc2 == null);
+        }
+
+        return loc2 != null && (loc1.getLineNr() == loc2.getLineNr() &&
+                loc1.getColumnNr() == loc2.getColumnNr() &&
+                loc1.getByteOffset() == loc2.getByteOffset() &&
+                loc1.getCharOffset() == loc2.getCharOffset());
+    }
+
+    /**
+     * Check wether the given data type contains an identifier property according
+     * to SIS convention (see {@link AttributeConvention#IDENTIFIER_PROPERTY}).
+     *
+     * @param toSearchIn The data type to scan for an identifier.
+     * @return True if an sis:identifier property is available. False otherwise.
+     */
+    public static boolean hasIdentifier(final FeatureType toSearchIn) {
+        try {
+            toSearchIn.getProperty(AttributeConvention.IDENTIFIER_PROPERTY.toString());
+            return true;
+        } catch (PropertyNotFoundException ex) {
+            return false;
+        }
+    }
+
+    /**
+     * If an sis:identifier property is available (see
+     * {@link AttributeConvention#IDENTIFIER_PROPERTY}), we try to acquire it
+     * value type (see {@link AttributeType#getValueClass() }. If we cannot
+     * determine the value type for this property, we simply return an empty
+     * optional. Note that an error is thrown if the given feature type does not
+     * contain any identifier property.
+     *
+     * @param toSearchIn The property to extract identifier from.
+     * @return The value class of found property if we can determine it (i.e:
+     * it's an attribute or an operation from which we can unravel an
+     * attribute), or an empty object if the property cannot provide value
+     * class.
+     * @throws PropertyNotFoundException If no
+     * {@link AttributeConvention#IDENTIFIER_PROPERTY} is present in the input.
+     */
+    public static Optional<Class> getIdentifierType(final FeatureType toSearchIn) throws PropertyNotFoundException {
+        final PropertyType idProperty = toSearchIn.getProperty(AttributeConvention.IDENTIFIER_PROPERTY.toString());
+        return castOrUnwrap(idProperty).map(AttributeType::getValueClass);
+    }
+
+    /**
+     * Create a converter to set values of arbitrary type into the sis:identifier
+     * property of a given feature type.
+     * Note: RFC7946 specifies that identifier must be either numeric or string.
+     *
+     * @param target The feature type which specifies the sis:identifier, and by
+     * extension the output value class for the converter to create.
+     * @return A function capable of converting arbitrary objects into required
+     * type for sis:identifier property.
+     * @throws IllegalArgumentException If the given data type provides a bad value
+     * class for identifier property.
+     */
+    public static Function getIdentifierConverter(final FeatureType target) throws IllegalArgumentException {
+        final Class identifierType = GeoJSONUtils.getIdentifierType(target)
+                .orElseThrow(() -> new IllegalArgumentException("Cannot determine the value type for identifier property. Should either be a string or a number."));
+        final Function converter;
+        if (Numbers.isFloat(identifierType)) {
+            converter = input -> Double.parseDouble(input.toString());
+        } else if (Long.class.isAssignableFrom(identifierType)) {
+            converter = input -> Long.parseLong(input.toString());
+        } else if (Numbers.isInteger(identifierType)) {
+            converter = input -> Integer.parseInt(input.toString());
+        } else if (String.class.isAssignableFrom(identifierType)) {
+            converter = Object::toString;
+        } else {
+            throw new IllegalArgumentException("Unsupported type for identifier property. RFC 7946 asks for a string or number data.");
+        }
+
+        return input -> {
+            if (input == null || identifierType.getClass().isAssignableFrom(input.getClass())) {
+                return input;
+            }
+
+            return converter.apply(input);
+        };
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeometryUtils.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeometryUtils.java
new file mode 100644
index 0000000..b41748f
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/GeometryUtils.java
@@ -0,0 +1,353 @@
+/*
+ * 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.sis.storage.geojson.utils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import org.apache.sis.storage.geojson.binding.*;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONGeometryCollection;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONLineString;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiLineString;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiPoint;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONMultiPolygon;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONPoint;
+import org.apache.sis.storage.geojson.binding.GeoJSONGeometry.GeoJSONPolygon;
+import org.apache.sis.util.Static;
+import org.locationtech.jts.geom.*;
+import org.locationtech.jts.geom.impl.CoordinateArraySequence;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public final class GeometryUtils extends Static {
+
+    private static final GeometryFactory GF = new GeometryFactory();
+
+    /**
+     * Convert GeoJSONGeometry into JTS Geometry with included CRS
+     * @param jsonGeometry
+     * @param crs
+     * @return JTS Geometry
+     */
+    public static Geometry toJTS(GeoJSONGeometry jsonGeometry, CoordinateReferenceSystem crs) {
+
+        if (jsonGeometry != null) {
+            if (crs == null) {
+                throw new IllegalArgumentException("Null Coordinate Reference System.");
+            }
+
+            if (jsonGeometry instanceof GeoJSONPoint) {
+                return toJTS((GeoJSONPoint) jsonGeometry, crs);
+            } else if (jsonGeometry instanceof GeoJSONLineString) {
+                return toJTS((GeoJSONLineString) jsonGeometry, crs);
+            } else if (jsonGeometry instanceof GeoJSONPolygon) {
+                return toJTS((GeoJSONPolygon) jsonGeometry, crs);
+            } else if (jsonGeometry instanceof GeoJSONMultiPoint) {
+                return toJTS((GeoJSONMultiPoint) jsonGeometry, crs);
+            } else if (jsonGeometry instanceof GeoJSONMultiLineString) {
+                return toJTS((GeoJSONMultiLineString) jsonGeometry, crs);
+            } else if (jsonGeometry instanceof GeoJSONMultiPolygon) {
+                return toJTS((GeoJSONMultiPolygon) jsonGeometry, crs);
+            } else if (jsonGeometry instanceof GeoJSONGeometryCollection) {
+                return toJTS((GeoJSONGeometryCollection) jsonGeometry, crs);
+            } else {
+                throw new IllegalArgumentException("Unsupported geometry type : " + jsonGeometry);
+            }
+        }
+        return null;
+    }
+
+    private static Coordinate toCoordinate(double[] coord) {
+        if (coord.length == 2) {
+            return new Coordinate(coord[0], coord[1]);
+        } else if (coord.length == 3) {
+            return new Coordinate(coord[0], coord[1], coord[2]);
+        } else {
+            throw new IllegalArgumentException("Coordinates not valid : " + Arrays.toString(coord));
+        }
+    }
+
+    private static CoordinateSequence toCoordinateSequence(double[][] coords) {
+
+        Coordinate[] coordinates = new Coordinate[coords.length];
+        if (coords.length > 0) {
+            for (int i = 0; i < coords.length; i++) {
+                coordinates[i] = toCoordinate(coords[i]);
+            }
+        }
+        return new CoordinateArraySequence(coordinates);
+    }
+
+    private static LinearRing toLinearRing(double[][] coords) {
+        return GF.createLinearRing(toCoordinateSequence(coords));
+    }
+
+    private static Polygon toPolygon(double[][][] coords, CoordinateReferenceSystem crs) {
+
+        LinearRing exterior = toLinearRing(coords[0]);
+        LinearRing[] holes = new LinearRing[coords.length-1];
+        if (coords.length > 1) {
+            for (int i = 0; i < holes.length; i++) {
+                holes[i] = toLinearRing(coords[i+1]);
+            }
+        }
+
+        Polygon polygon = GF.createPolygon(exterior, holes);
+        polygon.setUserData(crs);
+        return polygon;
+    }
+
+    private static Point toJTS(GeoJSONPoint jsonPoint, CoordinateReferenceSystem crs) {
+        double[] coord = jsonPoint.getCoordinates();
+
+        final Point pt = GF.createPoint(toCoordinate(coord));
+        pt.setUserData(crs);
+        return pt;
+    }
+
+    private static LineString toJTS(GeoJSONLineString jsonLS, CoordinateReferenceSystem crs) {
+        double[][] coord = jsonLS.getCoordinates();
+
+        LineString line = GF.createLineString(toCoordinateSequence(coord));
+        line.setUserData(crs);
+        return line;
+    }
+
+    private static Geometry toJTS(GeoJSONPolygon jsonPolygon, CoordinateReferenceSystem crs) {
+        double[][][] coord = jsonPolygon.getCoordinates();
+
+        if (coord.length <= 0) {
+            return GF.buildGeometry(Collections.EMPTY_LIST);
+        }
+
+        return toPolygon(coord, crs);
+    }
+
+    private static MultiPoint toJTS(GeoJSONMultiPoint jsonMP, CoordinateReferenceSystem crs) {
+        double[][] coords = jsonMP.getCoordinates();
+
+        Coordinate[] coordinates = new Coordinate[coords.length];
+        if (coords.length > 0) {
+            for (int i = 0; i < coords.length; i++) {
+                coordinates[i] = toCoordinate(coords[i]);
+            }
+        }
+
+        MultiPoint mpt = GF.createMultiPoint(GF.getCoordinateSequenceFactory().create(coordinates));
+        mpt.setUserData(crs);
+        return mpt;
+    }
+
+    private static MultiLineString toJTS(GeoJSONMultiLineString jsonMLS, CoordinateReferenceSystem crs) {
+        double[][][] coords = jsonMLS.getCoordinates();
+
+        LineString[] lines = new LineString[coords.length];
+        if (coords.length > 0) {
+            for (int i = 0; i < coords.length; i++) {
+                lines[i] = GF.createLineString(toCoordinateSequence(coords[i]));
+            }
+        }
+
+        MultiLineString mls = GF.createMultiLineString(lines);
+        mls.setUserData(crs);
+        return mls;
+    }
+
+    private static MultiPolygon toJTS(GeoJSONMultiPolygon jsonMP, CoordinateReferenceSystem crs) {
+        double[][][][] coords = jsonMP.getCoordinates();
+
+        Polygon[] polygons = new Polygon[coords.length];
+        if (coords.length > 0) {
+            for (int i = 0; i < coords.length; i++) {
+                polygons[i] = toPolygon(coords[i], crs);
+            }
+        }
+
+        MultiPolygon mp = GF.createMultiPolygon(polygons);
+        mp.setUserData(crs);
+        return mp;
+    }
+
+    private static GeometryCollection toJTS(GeoJSONGeometryCollection jsonGC, CoordinateReferenceSystem crs) {
+        if (jsonGC.getGeometries() != null) {
+
+            int size = jsonGC.getGeometries().size();
+            Geometry[] geometries = new Geometry[size];
+
+            for (int i = 0; i < size; i++) {
+                geometries[i] = toJTS(jsonGC.getGeometries().get(i), crs);
+            }
+
+            GeometryCollection gc = GF.createGeometryCollection(geometries);
+            gc.setUserData(crs);
+            return gc;
+        }
+        return null;
+    }
+
+
+    /**
+     * Convert JTS geometry into a GeoJSONGeometry.
+     * @param geom JTS Geometry
+     * @return GeoJSONGeometry
+     */
+    public static GeoJSONGeometry toGeoJSONGeometry(Geometry geom) {
+        if (geom == null) {
+            throw new IllegalArgumentException("Null Geometry.");
+        }
+
+        if (geom instanceof Point) {
+            return toGeoJSONGeometry((Point) geom);
+        } else if (geom instanceof LineString) {
+            return toGeoJSONGeometry((LineString) geom);
+        } else if (geom instanceof Polygon) {
+            return toGeoJSONGeometry((Polygon) geom);
+        } else if (geom instanceof MultiPoint) {
+            return toGeoJSONGeometry((MultiPoint) geom);
+        } else if (geom instanceof MultiLineString) {
+            return toGeoJSONGeometry((MultiLineString) geom);
+        } else if (geom instanceof MultiPolygon) {
+            return toGeoJSONGeometry((MultiPolygon) geom);
+        } else if (geom instanceof GeometryCollection) {
+            return toGeoJSONGeometry((GeometryCollection) geom);
+        } else {
+            throw new IllegalArgumentException("Unsupported geometry type : " + geom);
+        }
+    }
+
+    private static double[] toArray(Coordinate coord) {
+        double x = coord.getOrdinate(0);
+        double y = coord.getOrdinate(1);
+        //do not use getOrdinate for Z, may raise an exception
+        double z = coord.getZ();
+
+        if (Double.isNaN(z)) {
+            return new double[] {x, y};
+        } else {
+            return new double[] {x, y, z};
+        }
+    }
+
+    private static double[][] toArray(Coordinate[] coords) {
+        double[][] result = new double[coords.length][];
+
+        for (int i = 0; i < coords.length; i++) {
+            result[i] = toArray(coords[i]);
+        }
+        return result;
+    }
+
+    private static double[][] toArray(CoordinateSequence coords) {
+        return toArray(coords.toCoordinateArray());
+    }
+
+    private static double[][][] toArray(CoordinateSequence[] coords) {
+        double[][][] result = new double[coords.length][][];
+
+        for (int i = 0; i < coords.length; i++) {
+            result[i] = toArray(coords[i]);
+        }
+        return result;
+    }
+
+    private static double[][][][] toArray(CoordinateSequence[][] coords) {
+        double[][][][] result = new double[coords.length][][][];
+
+        for (int i = 0; i < coords.length; i++) {
+            result[i] = toArray(coords[i]);
+        }
+        return result;
+    }
+
+    private static GeoJSONPoint toGeoJSONGeometry(Point pt) {
+        GeoJSONPoint jsonPt = new GeoJSONPoint();
+        jsonPt.setCoordinates(toArray(pt.getCoordinate()));
+        return jsonPt;
+    }
+
+    private static GeoJSONLineString toGeoJSONGeometry(LineString line) {
+        GeoJSONLineString jsonln = new GeoJSONLineString();
+        jsonln.setCoordinates(toArray(line.getCoordinateSequence()));
+        return jsonln;
+    }
+
+    private static GeoJSONPolygon toGeoJSONGeometry(Polygon polygon) {
+        GeoJSONPolygon jsonpoly = new GeoJSONPolygon();
+        CoordinateSequence[] coords = getCoordinateSequencesFromPolygon(polygon);
+        jsonpoly.setCoordinates(toArray(coords));
+        return jsonpoly;
+    }
+
+    private static CoordinateSequence[] getCoordinateSequencesFromPolygon(Polygon polygon) {
+        int totalRings = polygon.getNumInteriorRing() + 1;
+        CoordinateSequence[] coords = new CoordinateSequence[totalRings];
+        coords[0] = polygon.getExteriorRing().getCoordinateSequence();
+
+        if (totalRings > 1) {
+            for (int i = 0; i < totalRings - 1; i++) {
+                coords[i+1] = polygon.getInteriorRingN(i).getCoordinateSequence();
+            }
+        }
+        return coords;
+    }
+
+    private static GeoJSONMultiPoint toGeoJSONGeometry(MultiPoint mpt) {
+        GeoJSONMultiPoint jsonMpt = new GeoJSONMultiPoint();
+        jsonMpt.setCoordinates(toArray(mpt.getCoordinates()));
+        return jsonMpt;
+    }
+
+    private static GeoJSONMultiLineString toGeoJSONGeometry(MultiLineString mln) {
+        GeoJSONMultiLineString jsonMln = new GeoJSONMultiLineString();
+        int totalRings = mln.getNumGeometries();
+        CoordinateSequence[] coords = new CoordinateSequence[totalRings];
+        for (int i = 0; i < totalRings; i++) {
+            coords[i] = ((LineString)mln.getGeometryN(i)).getCoordinateSequence();
+        }
+        jsonMln.setCoordinates(toArray(coords));
+        return jsonMln;
+    }
+
+    private static GeoJSONMultiPolygon toGeoJSONGeometry(MultiPolygon multiPolygon) {
+        GeoJSONMultiPolygon jsonMPoly = new GeoJSONMultiPolygon();
+        int totalPoly = multiPolygon.getNumGeometries();
+
+        CoordinateSequence[][] coords = new CoordinateSequence[totalPoly][];
+        for (int i = 0; i < totalPoly; i++) {
+            coords[i] = getCoordinateSequencesFromPolygon((Polygon) multiPolygon.getGeometryN(i));
+        }
+
+        jsonMPoly.setCoordinates(toArray(coords));
+        return jsonMPoly;
+    }
+
+    private static GeoJSONGeometryCollection toGeoJSONGeometry(GeometryCollection geometryCollection) {
+        GeoJSONGeometryCollection coll = new GeoJSONGeometryCollection();
+        int numGeometries = geometryCollection.getNumGeometries();
+
+        for (int i = 0; i < numGeometries; i++) {
+            coll.getGeometries().add(toGeoJSONGeometry(geometryCollection.getGeometryN(i)));
+        }
+
+        return coll;
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/LiteJsonLocation.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/LiteJsonLocation.java
new file mode 100644
index 0000000..1cb4636
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/utils/LiteJsonLocation.java
@@ -0,0 +1,104 @@
+/*
+ * 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.sis.storage.geojson.utils;
+
+import com.fasterxml.jackson.core.JsonLocation;
+
+import java.util.Objects;
+
+/**
+ * Lightweight pojo of {@link JsonLocation} without internal source object reference and offset.
+ * Because since 2.3.x+ of jackson byteOffset and charOffset values depend of underling
+ * source type. (InputStream to use byteOffset, BufferedReader to use charOffset)
+ *
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class LiteJsonLocation {
+
+    private final int lineNr;
+    private final int columnNr;
+
+    public LiteJsonLocation(JsonLocation location) {
+        this.lineNr = location.getLineNr();
+        this.columnNr = location.getColumnNr();
+    }
+
+    public int getLineNr() {
+        return lineNr;
+    }
+
+    public int getColumnNr() {
+        return columnNr;
+    }
+
+    /**
+     * Check if an JsonLocation position (line and column) is before
+     * current LiteJsonLocation.
+     * @param o JsonLocation
+     * @return true if before and false if input JsonLocation is equals or after current LiteJsonLocation
+     */
+    public boolean isBefore(JsonLocation o) {
+        if (o == null) return false;
+        LiteJsonLocation that = new LiteJsonLocation(o);
+
+        return lineNr < that.lineNr || (lineNr == that.lineNr  && columnNr < that.columnNr);
+    }
+
+    /**
+     * Test equality with LiteJsonLocation and JsonLocation input objects
+     *
+     * @param o
+     * @return
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+
+        // not equals if o is null or not an instance of LiteJsonLocation or JsonLocation
+        if (o == null ||
+                (!LiteJsonLocation.class.isAssignableFrom(o.getClass()) &&
+                        !JsonLocation.class.isAssignableFrom(o.getClass()))) return false;
+
+        LiteJsonLocation that;
+        if (JsonLocation.class.isAssignableFrom(o.getClass())) {
+            that = new LiteJsonLocation((JsonLocation) o);
+        } else {
+            that = (LiteJsonLocation) o;
+        }
+
+        return lineNr == that.lineNr &&
+                columnNr == that.columnNr;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(lineNr, columnNr);
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("LiteJsonLocation{");
+        sb.append("lineNr=").append(lineNr);
+        sb.append(", columnNr=").append(columnNr);
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/storage/sis-geojson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider b/storage/sis-geojson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider
new file mode 100644
index 0000000..33f5d70
--- /dev/null
+++ b/storage/sis-geojson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider
@@ -0,0 +1 @@
+org.apache.sis.storage.geojson.GeoJSONProvider
diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/FeatureTypeUtilsTest.java b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/FeatureTypeUtilsTest.java
new file mode 100644
index 0000000..4a2b1c0
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/FeatureTypeUtilsTest.java
@@ -0,0 +1,265 @@
+/*
+ * 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.sis.internal.storage.geojson;
+
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.util.iso.SimpleInternationalString;
+import org.junit.Test;
+import org.opengis.util.FactoryException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.sis.feature.builder.AttributeRole;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.storage.geojson.utils.FeatureTypeUtils;
+import org.apache.sis.storage.geojson.utils.GeoJSONUtils;
+import org.apache.sis.test.TestCase;
+
+import static org.junit.Assert.*;
+import org.opengis.feature.FeatureAssociationRole;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyNotFoundException;
+import org.opengis.feature.PropertyType;
+import org.opengis.util.GenericName;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class FeatureTypeUtilsTest extends TestCase {
+
+    public static void main(String[] args) throws Exception {
+       new FeatureTypeUtilsTest().writeReadFTTest();
+    }
+
+    @Test
+    public void writeReadFTTest() throws Exception {
+
+        Path featureTypeFile = Files.createTempFile("complexFT", ".json");
+
+        FeatureType featureType = createComplexType();
+        FeatureTypeUtils.writeFeatureType(featureType, featureTypeFile);
+
+        assertTrue(Files.size(featureTypeFile) > 0);
+
+        FeatureType readFeatureType = FeatureTypeUtils.readFeatureType(featureTypeFile);
+
+        assertNotNull(readFeatureType);
+        assertTrue(hasAGeometry(readFeatureType));
+        assertNotNull(GeoJSONUtils.getCRS(readFeatureType));
+
+        equalsIgnoreConvention(featureType, readFeatureType);
+    }
+
+    @Test
+    public void writeReadNoCRSFTTest() throws Exception {
+
+        Path featureTypeFile = Files.createTempFile("geomFTNC", ".json");
+
+        FeatureType featureType = createGeometryNoCRSFeatureType();
+        FeatureTypeUtils.writeFeatureType(featureType, featureTypeFile);
+
+        assertTrue(Files.size(featureTypeFile) > 0);
+
+        FeatureType readFeatureType = FeatureTypeUtils.readFeatureType(featureTypeFile);
+
+        assertNotNull(readFeatureType);
+        assertTrue(hasAGeometry(readFeatureType));
+        assertNull(GeoJSONUtils.getCRS(readFeatureType));
+
+        equalsIgnoreConvention(featureType, readFeatureType);
+    }
+
+    @Test
+    public void writeReadCRSFTTest() throws Exception {
+
+        Path featureTypeFile = Files.createTempFile("geomFTC", ".json");
+
+        FeatureType featureType = createGeometryCRSFeatureType();
+        FeatureTypeUtils.writeFeatureType(featureType, featureTypeFile);
+
+        assertTrue(Files.size(featureTypeFile) > 0);
+
+        FeatureType readFeatureType = FeatureTypeUtils.readFeatureType(featureTypeFile);
+
+        assertNotNull(readFeatureType);
+        assertTrue(hasAGeometry(readFeatureType));
+        assertNotNull(GeoJSONUtils.getCRS(readFeatureType));
+
+        equalsIgnoreConvention(featureType, readFeatureType);
+    }
+
+    public static FeatureType createComplexType() throws FactoryException {
+        FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+
+        ftb.setName("complexAtt1");
+        ftb.addAttribute(Long.class).setName("longProp2");
+        ftb.addAttribute(String.class).setName("stringProp2");
+        final FeatureType complexAtt1 = ftb.build();
+
+        ftb = new FeatureTypeBuilder();
+        ftb.setName("complexAtt2");
+        ftb.addAttribute(Long.class).setName("longProp2");
+        ftb.addAttribute(Date.class).setName("dateProp");
+        final FeatureType complexAtt2 = ftb.build();
+
+        ftb = new FeatureTypeBuilder();
+        ftb.setName("complexFT");
+        ftb.addAttribute(Polygon.class).setName("geometry").setCRS(CommonCRS.WGS84.geographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        ftb.addAttribute(Long.class).setName("longProp");
+        ftb.addAttribute(String.class).setName("stringProp");
+        ftb.addAttribute(Integer.class).setName("integerProp");
+        ftb.addAttribute(Boolean.class).setName("booleanProp");
+        ftb.addAttribute(Date.class).setName("dateProp");
+
+        ftb.addAssociation(complexAtt1).setName("complexAtt1");
+        ftb.addAssociation(complexAtt2).setName("complexAtt2").setMinimumOccurs(0).setMaximumOccurs(Integer.MAX_VALUE);
+        ftb.setDescription(new SimpleInternationalString("Description"));
+        return ftb.build();
+    }
+
+    private FeatureType createGeometryNoCRSFeatureType() {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName("FT1");
+        ftb.addAttribute(Point.class).setName("geometry").addRole(AttributeRole.DEFAULT_GEOMETRY);
+        ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY);
+        ftb.addAttribute(String.class).setName("type");
+
+        return ftb.build();
+    }
+
+    private FeatureType createGeometryCRSFeatureType() {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName("FT2");
+        ftb.addAttribute(Point.class).setName("geometry").setCRS(CommonCRS.WGS84.geographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY);
+        ftb.addAttribute(String.class).setName("type");
+
+        return ftb.build();
+    }
+
+    /**
+     * Loop on properties, returns true if there is at least one geometry property.
+     *
+     * @param type
+     * @return true if type has a geometry.
+     */
+    public static boolean hasAGeometry(FeatureType type){
+        for (PropertyType pt : type.getProperties(true)){
+            if (AttributeConvention.isGeometryAttribute(pt)) return true;
+        }
+        return false;
+    }
+
+
+    /**
+     * Test field equality ignoring convention properties.
+     */
+    public static boolean equalsIgnoreConvention(FeatureType type1, FeatureType type2){
+
+        if (type1 == type2) {
+            return true;
+        }
+
+        //check base properties
+        if (!Objects.equals(type1.getName(),        type2.getName()) ||
+            !Objects.equals(type1.getDefinition(),  type2.getDefinition()) ||
+            !Objects.equals(type1.getDesignation(), type2.getDesignation()) ||
+            !Objects.equals(type1.getDesignation(), type2.getDesignation()) ||
+            !Objects.equals(type1.isAbstract(),     type2.isAbstract())){
+            return false;
+        }
+
+        //check super types
+        final Set<? extends FeatureType> super1 = type1.getSuperTypes();
+        final Set<? extends FeatureType> super2 = type2.getSuperTypes();
+        if(super1.size() != super2.size()) return false;
+        final Iterator<? extends FeatureType> site1 = super1.iterator();
+        final Iterator<? extends FeatureType> site2 = super2.iterator();
+        while(site1.hasNext()){
+            if(!equalsIgnoreConvention(site1.next(), site2.next())) return false;
+        }
+
+        //check properties
+        final Set<GenericName> visited = new HashSet<>();
+        for (PropertyType pt1 : type1.getProperties(true)) {
+            visited.add(pt1.getName());
+            if (AttributeConvention.contains(pt1.getName())) continue;
+            try {
+                final PropertyType pt2 = type2.getProperty(pt1.getName().toString());
+                if (!equalsIgnoreConvention(pt1, pt2)) return false;
+            } catch (PropertyNotFoundException ex) {
+                return false;
+            }
+        }
+
+        for (PropertyType pt2 : type2.getProperties(true)) {
+            if (AttributeConvention.contains(pt2.getName()) || visited.contains(pt2.getName())) continue;
+            try {
+                final PropertyType pt1 = type1.getProperty(pt2.getName().toString());
+                if (!equalsIgnoreConvention(pt1, pt2)) return false;
+            } catch (PropertyNotFoundException ex) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private static boolean equalsIgnoreConvention(PropertyType pt1, PropertyType pt2){
+        if(pt1 instanceof FeatureAssociationRole){
+            if(pt2 instanceof FeatureAssociationRole){
+                final FeatureAssociationRole far1 = (FeatureAssociationRole) pt1;
+                final FeatureAssociationRole far2 = (FeatureAssociationRole) pt2;
+
+                 //check base properties
+                if (!Objects.equals(far1.getName(),        far2.getName()) ||
+                    !Objects.equals(far1.getDefinition(),  far2.getDefinition()) ||
+                    !Objects.equals(far1.getDesignation(), far2.getDesignation()) ||
+                    !Objects.equals(far1.getDesignation(), far2.getDesignation())){
+                    return false;
+                }
+
+                if(far1.getMinimumOccurs()!=far2.getMinimumOccurs()||
+                   far1.getMaximumOccurs()!=far2.getMaximumOccurs()){
+                    return false;
+                }
+
+                if(!equalsIgnoreConvention(far1.getValueType(), far2.getValueType())){
+                    return false;
+                }
+
+            }else{
+                return false;
+            }
+        }else if(!pt1.equals(pt2)){
+            return false;
+        }
+        return true;
+    }
+
+}
diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONReadTest.java b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONReadTest.java
new file mode 100644
index 0000000..b9e1831
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONReadTest.java
@@ -0,0 +1,351 @@
+/*
+ * 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.sis.internal.storage.geojson;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Iterator;
+import org.apache.sis.feature.builder.AttributeRole;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStores;
+import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.geojson.GeoJSONStore;
+import org.apache.sis.storage.geojson.binding.GeoJSONFeatureCollection;
+import org.apache.sis.storage.geojson.binding.GeoJSONObject;
+import org.apache.sis.storage.geojson.utils.GeoJSONParser;
+import org.apache.sis.test.TestCase;
+import org.apache.sis.util.iso.Names;
+import static org.junit.Assert.*;
+import org.junit.Test;
+import org.locationtech.jts.geom.*;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
+import org.opengis.util.GenericName;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONReadTest extends TestCase {
+
+    @Test
+    public void readPointTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/point.json");
+
+        GeoJSONStore store = (GeoJSONStore) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "point"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("point", Point.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readMultiPointTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/multipoint.json");
+
+        GeoJSONStore store = (GeoJSONStore) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "multipoint"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("multipoint", MultiPoint.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readLineStringTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/linestring.json");
+
+        GeoJSONStore store = (GeoJSONStore) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "linestring"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("linestring", LineString.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readMultiLineStringTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/multilinestring.json");
+
+        GeoJSONStore store = (GeoJSONStore) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "multilinestring"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("multilinestring", MultiLineString.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readPolygonTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/polygon.json");
+
+        GeoJSONStore store = (GeoJSONStore) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "polygon"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("polygon", Polygon.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readMultiPolygonTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/multipolygon.json");
+
+        GeoJSONStore store = (GeoJSONStore) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "multipolygon"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("multipolygon", MultiPolygon.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readGeometryCollectionTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/geometrycollection.json");
+
+        GeoJSONStore store = (GeoJSONStore) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "geometrycollection"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("geometrycollection", GeometryCollection.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+
+    }
+
+    @Test
+    public void readFeatureTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/feature.json");
+
+        GeoJSONStore store = (GeoJSONStore) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "feature"), name);
+
+        testFeatureTypes(buildSimpleFeatureType("feature"), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readFeatureCollectionTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/featurecollection.json");
+
+        GeoJSONStore store = (GeoJSONStore) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "featurecollection"), name);
+
+        testFeatureTypes(buildFCFeatureType("featurecollection"), ft);
+
+        assertEquals(7l, store.features(false).count());
+    }
+
+    /**
+     * Test reading of Features with array as properties value
+     * @throws DataStoreException
+     */
+    @Test
+    public void readPropertyArrayTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/f_prop_array.json");
+
+        GeoJSONStore store = (GeoJSONStore) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "f_prop_array"), name);
+
+        testFeatureTypes(buildPropertyArrayFeatureType("f_prop_array", Geometry.class), ft);
+
+        assertEquals(2l, store.features(false).count());
+
+        Double[][] array1 = new Double[5][5];
+        Double[][] array2 = new Double[5][5];
+        for (int i = 0; i < 5; i++) {
+            for (int j = 0; j < 5; j++) {
+                array1[i][j] = (double) (i + j);
+                array2[i][j] = (double) (i - j);
+            }
+        }
+
+        Iterator<Feature> ite = store.features(false).iterator();
+        Feature feat1 = ite.next();
+        assertArrayEquals(array1, (Double[][]) feat1.getProperty("array").getValue());
+
+        Feature feat2 = ite.next();
+        assertArrayEquals(array2, (Double[][]) feat2.getProperty("array").getValue());
+
+    }
+
+    /**
+     * This test ensure that properties fields with null value doesn't rise NullPointerException
+     * @throws DataStoreException
+     */
+    @Test
+    public void readNullPropsTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/sample_with_null_properties.json");
+
+        GeoJSONStore store = (GeoJSONStore) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+
+        assertEquals(15l, store.features(false).count());
+    }
+
+    /**
+     * This test ensure integer types over Integer.MAX_VALUE are converted to Long.
+     * @throws DataStoreException
+     */
+    @Test
+    public void readLongTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/longValue.json");
+
+        GeoJSONStore store = (GeoJSONStore) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+
+        Feature feature = store.features(false).findFirst().get();
+        assertEquals(853555090789l, feature.getPropertyValue("size"));
+    }
+
+    /**
+     * Test GeoJSONParser full and lazy reading on FeatureCollection
+     */
+    @Test
+    public void parserTest() throws URISyntaxException, IOException {
+        URL fcFile = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/featurecollection.json");
+        Path fcPath = Paths.get(fcFile.toURI());
+
+        // test with full reading
+        GeoJSONObject geoJSONObject = GeoJSONParser.parse(fcPath, false);
+        assertTrue(geoJSONObject instanceof GeoJSONFeatureCollection);
+        GeoJSONFeatureCollection geojsonFC = (GeoJSONFeatureCollection) geoJSONObject;
+        assertFalse(geojsonFC.isLazyMode());
+        assertEquals(7, geojsonFC.getFeatures().size());
+
+        for (int i = 0; i < 7; i++) {
+            assertTrue(geojsonFC.hasNext());
+            assertNotNull(geojsonFC.next());
+        }
+        assertFalse(geojsonFC.hasNext()); //end of collection
+
+
+        // test in lazy reading
+        geoJSONObject = GeoJSONParser.parse(fcPath, true);
+        assertTrue(geoJSONObject instanceof GeoJSONFeatureCollection);
+        geojsonFC = (GeoJSONFeatureCollection) geoJSONObject;
+        assertTrue(geojsonFC.isLazyMode());
+        assertEquals(0, geojsonFC.getFeatures().size()); //lazy don't know number of features
+
+        for (int i = 0; i < 7; i++) {
+            assertTrue(geojsonFC.hasNext());
+            assertNotNull(geojsonFC.next());
+        }
+        assertFalse(geojsonFC.hasNext()); //end of collection
+
+    }
+
+    private FeatureType buildPropertyArrayFeatureType(String name, Class<?> geomClass) {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(Double[][].class).setName("array");
+        ftb.addAttribute(geomClass).setName(AttributeConvention.GEOMETRY_PROPERTY).setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        return ftb.build();
+    }
+
+    private FeatureType buildGeometryFeatureType(String name, Class<?> geomClass) {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(geomClass).setName(AttributeConvention.GEOMETRY_PROPERTY).setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        return ftb.build();
+    }
+
+    private FeatureType buildSimpleFeatureType(String name) {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(Polygon.class).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        ftb.addAttribute(String.class).setName("name");
+        return ftb.build();
+    }
+
+    private FeatureType buildFCFeatureType(String name) {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(Geometry.class).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        ftb.addAttribute(String.class).setName("name");
+        ftb.addAttribute(String.class).setName("address");
+        return ftb.build();
+    }
+
+    private void testFeatureTypes(FeatureType expected, FeatureType result) {
+        for(PropertyType desc : expected.getProperties(true)){
+            PropertyType td = result.getProperty(desc.getName().toString());
+            assertNotNull(td);
+            if(td instanceof AttributeType){
+                assertEquals(((AttributeType) td).getValueClass(), ((AttributeType)desc).getValueClass());
+            }
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONWriteTest.java b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONWriteTest.java
new file mode 100644
index 0000000..d4894c8
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONWriteTest.java
@@ -0,0 +1,577 @@
+/*
+ * 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.sis.internal.storage.geojson;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import java.io.*;
+import java.lang.reflect.Array;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.stream.Stream;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.feature.builder.AttributeRole;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.geojson.GeoJSONProvider;
+import org.apache.sis.storage.geojson.GeoJSONStreamWriter;
+import org.apache.sis.util.iso.SimpleInternationalString;
+import org.apache.sis.storage.geojson.GeoJSONStore;
+import org.apache.sis.test.TestCase;
+import static org.junit.Assert.*;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.locationtech.jts.geom.*;
+import org.locationtech.jts.io.WKTReader;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureAssociationRole;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONWriteTest extends TestCase {
+
+    private static final GeometryFactory GF = new GeometryFactory();
+    private static final WKTReader WKT_READER = new WKTReader();
+    private static final Properties PROPERTIES = new Properties();
+
+    @BeforeClass
+    public static void init() throws IOException {
+        PROPERTIES.load(GeoJSONWriteTest.class.getResourceAsStream("/org/apache/sis/internal/storage/geojson/geometries.properties"));
+    }
+
+    @Test
+    public void writeSimpleFTTest() throws Exception {
+
+        final Path file = Files.createTempFile("point", ".json");
+
+        final GeoJSONStore store = new GeoJSONStore(new GeoJSONProvider(), file, 7);
+        assertNotNull(store);
+        final String typeName = file.getFileName().toString().replace(".json", "");
+
+
+        //test creating an unvalid feature type
+        final FeatureType unvalidFeatureType = buildGeometryFeatureType("test", Point.class);
+        try {
+            store.updateType(unvalidFeatureType);
+            fail();
+        } catch (DataStoreException ex) {
+            //normal exception
+        }
+
+
+        //test writing and reading a feature
+        final FeatureType validFeatureType = buildGeometryFeatureType(typeName, Point.class);
+        store.updateType(validFeatureType);
+        assertNotNull(store.getType());
+        assertTrue(Files.exists(file));
+
+        final Point expectedPoint = GF.createPoint(new Coordinate(-105.01621, 39.57422));
+        final Feature feature = store.getType().newInstance();
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), expectedPoint);
+        feature.setPropertyValue("type","simple");
+        store.add(Arrays.asList(feature).iterator());
+
+        assertTrue(Files.exists(file));
+
+
+        try (Stream<Feature> stream = store.features(false)) {
+            final Iterator<Feature> reader = stream.iterator();
+            assertTrue(reader.hasNext());
+            final Feature f = reader.next();
+            assertEquals("simple", f.getPropertyValue("type"));
+            assertEquals(expectedPoint, f.getPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString()));
+        }
+
+        Files.deleteIfExists(file);
+    }
+
+    @Test
+    public void writeAbstractGeometryTest() throws Exception {
+
+        Path file = Files.createTempFile("geoms", ".json");
+
+        GeoJSONStore store = new GeoJSONStore(new GeoJSONProvider(), file, 7);
+        assertNotNull(store);
+
+        String typeName = file.getFileName().toString().replace(".json", "");
+        FeatureType validFeatureType = buildGeometryFeatureType(typeName, Geometry.class);
+
+        store.updateType(validFeatureType);
+        assertNotNull(store.getType());
+        assertTrue(Files.exists(file));
+
+        Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point"));
+        MultiPoint mpt = (MultiPoint)WKT_READER.read(PROPERTIES.getProperty("multipoint"));
+        LineString line = (LineString)WKT_READER.read(PROPERTIES.getProperty("linestring"));
+        MultiLineString mline = (MultiLineString)WKT_READER.read(PROPERTIES.getProperty("multilinestring"));
+        Polygon poly = (Polygon)WKT_READER.read(PROPERTIES.getProperty("polygon"));
+        MultiPolygon mpoly = (MultiPolygon)WKT_READER.read(PROPERTIES.getProperty("multipolygon"));
+        GeometryCollection coll = (GeometryCollection)WKT_READER.read(PROPERTIES.getProperty("geometrycollection"));
+
+        Feature feature = store.getType().newInstance();
+        feature.setPropertyValue("type","Point");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt);
+        store.add(Arrays.asList(feature).iterator());
+
+        feature = store.getType().newInstance();
+        feature.setPropertyValue("type","MultiPoint");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), mpt);
+        store.add(Arrays.asList(feature).iterator());
+
+        feature = store.getType().newInstance();
+        feature.setPropertyValue("type","LineString");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), line);
+        store.add(Arrays.asList(feature).iterator());
+
+        feature = store.getType().newInstance();
+        feature.setPropertyValue("type","MultiLineString");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), mline);
+        store.add(Arrays.asList(feature).iterator());
+
+        feature = store.getType().newInstance();
+        feature.setPropertyValue("type","Polygon");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), poly);
+        store.add(Arrays.asList(feature).iterator());
+
+        feature = store.getType().newInstance();
+        feature.setPropertyValue("type","MultiPolygon");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), mpoly);
+        store.add(Arrays.asList(feature).iterator());
+
+        feature = store.getType().newInstance();
+        feature.setPropertyValue("type","GeometryCollection");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), coll);
+        store.add(Arrays.asList(feature).iterator());
+
+        assertTrue(Files.exists(file));
+
+        assertEquals(7, store.features(false).count());
+
+        try (Stream<Feature> stream = store.features(false)) {
+            Iterator<Feature> ite = stream.iterator();
+            while (ite.hasNext()) {
+                Feature f = ite.next();
+                //System.out.println(f);
+                Geometry geom = (Geometry)f.getPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString());
+
+                if (geom instanceof Point) {
+                    assertTrue(pt.equalsExact(geom, 0.0000001));
+                } else if (geom instanceof MultiPoint) {
+                    assertTrue(mpt.equalsExact(geom, 0.0000001));
+                } else if (geom instanceof LineString) {
+                    assertTrue(line.equalsExact(geom, 0.0000001));
+                } else if (geom instanceof MultiLineString) {
+                    assertTrue(mline.equalsExact(geom, 0.0000001));
+                } else if (geom instanceof Polygon) {
+                    assertTrue(poly.equalsExact(geom, 0.0000001));
+                } else if (geom instanceof MultiPolygon) {
+                    assertTrue(mpoly.equalsExact(geom, 0.0000001));
+                } else if (geom instanceof GeometryCollection) {
+                    assertTrue(coll.equalsExact(geom, 0.0000001));
+                }
+            }
+        }
+
+        Files.deleteIfExists(file);
+    }
+
+    @Test
+    public void writeComplexFeaturesTest() throws Exception {
+        Path file = Files.createTempFile("complex", ".json");
+
+        GeoJSONStore store = new GeoJSONStore(new GeoJSONProvider(), file, 7);
+        assertNotNull(store);
+
+        String typeName = file.getFileName().toString().replace(".json", "");
+
+        FeatureType complexFT = buildComplexFeatureType(typeName);
+
+        store.updateType(complexFT);
+        assertNotNull(store.getType());
+        assertTrue(Files.exists(file));
+
+        Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point"));
+        Feature expected = null;
+        Feature feature = store.getType().newInstance();
+        feature.setPropertyValue("longProp",100l);
+        feature.setPropertyValue("stringProp","Some String");
+        feature.setPropertyValue("integerProp",15);
+        feature.setPropertyValue("booleanProp",true);
+
+        final FeatureType level1Type = ((FeatureAssociationRole)feature.getType().getProperty("level1")).getValueType();
+
+        final Feature level11 = level1Type.newInstance();
+        level11.setPropertyValue("longProp2",66446l);
+
+        final FeatureType level2Type = ((FeatureAssociationRole)level11.getType().getProperty("level2")).getValueType();
+
+        final Feature level211 = level2Type.newInstance();
+        level211.setPropertyValue("level2prop","text");
+        final Feature level212 = level2Type.newInstance();
+        level212.setPropertyValue("level2prop","text2");
+        final Feature level213 = level2Type.newInstance();
+        level213.setPropertyValue("level2prop","text3");
+
+        level11.setPropertyValue("level2", Arrays.asList(level211,level212,level213));
+
+
+        Feature level12 = level1Type.newInstance();
+        level12.setPropertyValue("longProp2",4444444l);
+
+        final Feature level221 = level2Type.newInstance();
+        level221.setPropertyValue("level2prop","fish");
+        final Feature level222 = level2Type.newInstance();
+        level222.setPropertyValue("level2prop","cat");
+        final Feature level223 = level2Type.newInstance();
+        level223.setPropertyValue("level2prop","dog");
+
+        level12.setPropertyValue("level2", Arrays.asList(level221,level222,level223));
+
+        feature.setPropertyValue("level1",Arrays.asList(level11,level12));
+
+        feature.setPropertyValue("geometry", pt);
+        expected = copy(feature);
+        store.add(Arrays.asList(feature).iterator());
+
+
+        assertTrue(Files.exists(file));
+
+        assertEquals(1, store.features(false).count());
+
+        try (Stream<Feature> stream = store.features(false)) {
+            Iterator<Feature> ite = stream.iterator();
+            while (ite.hasNext()) {
+                Feature candidate = ite.next();
+                assertEquals(expected, candidate);
+            }
+        }
+        Files.deleteIfExists(file);
+    }
+
+    @Test
+    public void writeStreamTest() throws Exception {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        FeatureType validFeatureType = buildGeometryFeatureType("simpleFT", Point.class);
+
+        Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point"));
+
+        try (GeoJSONStreamWriter fw = new GeoJSONStreamWriter(baos, validFeatureType, 4)) {
+            Feature feature = fw.next();
+            feature.setPropertyValue("type","feat1");
+            feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt);
+            fw.write();
+
+            feature = fw.next();
+            feature.setPropertyValue("type","feat2");
+            feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt);
+            fw.write();
+
+        }
+
+        String outputJSON = baos.toString("UTF-8");
+        assertNotNull(outputJSON);
+        assertFalse(outputJSON.isEmpty());
+
+        String expected = "{\n" +
+                "\"type\":\"FeatureCollection\"\n" +
+                ",\"features\":[\n" +
+                "{\"type\":\"Feature\",\"id\":0,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]},\"properties\":{\"type\":\"feat1\"}}\n" +
+                ",{\"type\":\"Feature\",\"id\":1,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]},\"properties\":{\"type\":\"feat2\"}}\n" +
+                "]}";
+
+        assertEquals(expected, outputJSON);
+    }
+
+    @Test
+    public void writeStreamSingleFeatureTest() throws Exception {
+        FeatureType validFeatureType = buildGeometryFeatureType("simpleFT", Point.class);
+
+        Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point"));
+
+        final String outputJSON;
+        try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            Feature feature = validFeatureType.newInstance();
+            feature.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), 0);
+            feature.setPropertyValue("type","feat1");
+            feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt);
+            GeoJSONStreamWriter.writeSingleFeature(baos, feature, JsonEncoding.UTF8, 4, false);
+
+            outputJSON = baos.toString("UTF-8");
+        }
+
+        assertNotNull(outputJSON);
+        assertFalse(outputJSON.isEmpty());
+
+        String expected = "{\"type\":\"Feature\",\"id\":0," +
+                "\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]}," +
+                "\"properties\":{\"type\":\"feat1\"}}";
+        assertEquals(expected, outputJSON);
+    }
+
+    @Test
+    public void writeStreamSingleGeometryTest() throws Exception {
+        Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point"));
+
+        final String outputJSON;
+        try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            GeoJSONStreamWriter.writeSingleGeometry(baos, pt, JsonEncoding.UTF8, 4, false);
+
+            outputJSON = baos.toString("UTF-8");
+        }
+
+        assertNotNull(outputJSON);
+        assertFalse(outputJSON.isEmpty());
+
+        String expected = "{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]}";
+        assertEquals(expected, outputJSON);
+    }
+
+    @Test
+    public void writePropertyArrayTest() throws Exception {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        FeatureType validFeatureType = buildPropertyArrayFeatureType("arrayFT", Point.class);
+
+        Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point"));
+
+        double[][] array1 = new double[5][5];
+        double[][] array2 = new double[5][5];
+        for (int i = 0; i < 5; i++) {
+            for (int j = 0; j < 5; j++) {
+                array1[i][j] = i+j;
+                array2[i][j] = i-j;
+            }
+        }
+
+        try (GeoJSONStreamWriter fw = new GeoJSONStreamWriter(baos, validFeatureType, 4)) {
+            Feature feature = fw.next();
+            feature.setPropertyValue("array",array1);
+            feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt);
+            fw.write();
+
+            feature = fw.next();
+            feature.setPropertyValue("array",array2);
+            feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt);
+            fw.write();
+
+        }
+
+        String outputJSON = baos.toString("UTF-8");
+        assertNotNull(outputJSON);
+        assertFalse(outputJSON.isEmpty());
+
+        String expected = "{\n" +
+                "\"type\":\"FeatureCollection\"\n" +
+                ",\"features\":[\n" +
+                "{\"type\":\"Feature\",\"id\":\"0\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]},\"properties\":{\"array\":[[0.0,1.0,2.0,3.0,4.0],[1.0,2.0,3.0,4.0,5.0],[2.0,3.0,4.0,5.0,6.0],[3.0,4.0,5.0,6.0,7.0],[4.0,5.0,6.0,7.0,8.0]]}}\n" +
+                ",{\"type\":\"Feature\",\"id\":\"1\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]},\"properties\":{\"array\":[[0.0,-1.0,-2.0,-3.0,-4.0],[1.0,0.0,-1.0,-2.0,-3.0],[2.0,1.0,0.0,-1.0,-2.0],[3.0,2.0,1.0,0.0,-1.0],[4.0,3.0,2.0,1.0,0.0]]}}\n" +
+                "]}";
+        assertEquals(expected, outputJSON);
+    }
+
+    private FeatureType buildGeometryFeatureType(String name, Class<?> geomClass) {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(Integer.class).setName(AttributeConvention.IDENTIFIER_PROPERTY);
+        ftb.addAttribute(String.class).setName("type");
+        ftb.addAttribute(geomClass).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        return ftb.build();
+    }
+
+    private FeatureType buildPropertyArrayFeatureType(String name, Class<?> geomClass) {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY);
+        ftb.addAttribute(double[][].class).setName("array");
+        ftb.addAttribute(geomClass).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        return ftb.build();
+    }
+
+    /**
+     *  Build 2 level Feature complex
+     */
+    private FeatureType buildComplexFeatureType(String name) {
+        FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+
+        ftb.setName("level2");
+        ftb.addAttribute(String.class).setName("level2prop");
+        final FeatureType level2 = ftb.build();
+
+        ftb = new FeatureTypeBuilder();
+        ftb.setName("level1");
+        ftb.addAttribute(Long.class).setName("longProp2");
+        ftb.addAssociation(level2).setName("level2").setMinimumOccurs(1).setMaximumOccurs(5);
+        final FeatureType level1 = ftb.build();
+
+        ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY);
+        ftb.addAttribute(Long.class).setName("longProp");
+        ftb.addAttribute(String.class).setName("stringProp");
+        ftb.addAttribute(Integer.class).setName("integerProp");
+        ftb.addAttribute(Boolean.class).setName("booleanProp");
+        ftb.addAssociation(level1).setName("level1").setMinimumOccurs(1).setMaximumOccurs(3);
+        ftb.addAttribute(Point.class).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        ftb.setDescription(new SimpleInternationalString("Description"));
+        return ftb.build();
+    }
+
+    /**
+     * Create a copy of given feature.
+     * This is not a deep copy, only the feature and associated feature are copied,
+     * values are not copied.
+     */
+    public static Feature copy(Feature feature){
+        return copy(feature, false);
+    }
+
+    /**
+     * @param deep true for a deep copy
+     */
+    private static Feature copy(Feature feature, boolean deep){
+        final FeatureType type = feature.getType();
+
+        final Feature cp = type.newInstance();
+
+        final Collection<? extends PropertyType> props = type.getProperties(true);
+        for (PropertyType pt : props) {
+            if (pt instanceof AttributeType ){
+                final String name = pt.getName().toString();
+                final Object val = feature.getPropertyValue(name);
+                if(val!=null){
+                    cp.setPropertyValue(name, deep ? deepCopy(val) : val);
+                }
+            } else if(pt instanceof FeatureAssociationRole) {
+                final String name = pt.getName().toString();
+                final Object val = feature.getPropertyValue(name);
+                if (deep) {
+                    if(val!=null){
+                        cp.setPropertyValue(name, deepCopy(val));
+                    }
+                } else {
+                    if(val instanceof Collection){
+                        final Collection col = (Collection) val;
+                        final Collection cpCol = new ArrayList(col.size());
+                        for(Iterator ite=col.iterator();ite.hasNext();){
+                            cpCol.add(copy((Feature)ite.next()));
+                        }
+                        cp.setPropertyValue(name, cpCol);
+                    }else if(val!=null){
+                        cp.setPropertyValue(name, copy((Feature)val));
+                    }
+                }
+
+            }
+        }
+        return cp;
+    }
+
+    /**
+     * Make a deep copy of given Feature.
+     *
+     * @param feature Feature to copy
+     * @return Deep copy of the feature
+     */
+    public static Feature deepCopy(Feature feature){
+        return copy(feature, true);
+    }
+
+    /**
+     * Make a copy of given object.
+     * Multiplace cases are tested to make a deep copy.
+     *
+     * @param candidate
+     * @return copied object
+     */
+    public static Object deepCopy(final Object candidate) {
+        if(candidate==null) return null;
+
+        if(candidate instanceof String ||
+           candidate instanceof Number ||
+           candidate instanceof URL ||
+           candidate instanceof URI ||
+           candidate.getClass().isPrimitive() ||
+           candidate instanceof Character ||
+           candidate instanceof GridCoverage){
+            //we consider those immutable
+            return candidate;
+        }else if(candidate instanceof Feature){
+            return deepCopy((Feature)candidate);
+        }else if(candidate instanceof Geometry){
+            return ((Geometry)candidate).clone();
+        }else if(candidate instanceof Date){
+            return ((Date)candidate).clone();
+        }else if(candidate instanceof Date){
+            return ((Date)candidate).clone();
+        }else if(candidate instanceof Object[]){
+            final Object[] array = (Object[])candidate;
+            final Object[] copy = new Object[array.length];
+            for (int i = 0; i < array.length; i++) {
+                copy[i] = deepCopy(array[i]);
+            }
+            return copy;
+        }else if(candidate instanceof List){
+            final List list = (List)candidate;
+            final int size = list.size();
+            final List cp = new ArrayList(size);
+            for(int i=0;i<size;i++){
+                cp.add(deepCopy(list.get(i)));
+            }
+            return cp;
+        }else if (candidate instanceof Map) {
+            final Map map = (Map)candidate;
+            final Map cp = new HashMap(map.size());
+            for(final Iterator<Map.Entry> ite=map.entrySet().iterator(); ite.hasNext();) {
+                final Map.Entry entry = ite.next();
+                cp.put(entry.getKey(), deepCopy(entry.getValue()));
+            }
+            return Collections.unmodifiableMap(cp);
+        }
+
+        //array type
+        final Class clazz = candidate.getClass();
+        if(clazz.isArray()){
+            final Class compClazz = clazz.getComponentType();
+            final int length = Array.getLength(candidate);
+            final Object cp = Array.newInstance(compClazz, length);
+
+            if(compClazz.isPrimitive()){
+                System.arraycopy(candidate, 0, cp, 0, length);
+            }else{
+                for(int i=0;i<length; i++){
+                    Array.set(cp, i, deepCopy(Array.get(candidate, i)));
+                }
+            }
+            return cp;
+        }
+
+        //could not copy
+        return candidate;
+    }
+
+}
diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/LiteJsonLocationTest.java b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/LiteJsonLocationTest.java
new file mode 100644
index 0000000..1043c1c
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/LiteJsonLocationTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.sis.internal.storage.geojson;
+
+import com.fasterxml.jackson.core.JsonLocation;
+import com.fasterxml.jackson.core.JsonParser;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.apache.sis.storage.geojson.utils.GeoJSONParser;
+import org.apache.sis.storage.geojson.utils.GeoJSONUtils;
+import org.apache.sis.storage.geojson.utils.LiteJsonLocation;
+import org.apache.sis.test.TestCase;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class LiteJsonLocationTest extends TestCase {
+
+    @Test
+    public void testEquality () throws URISyntaxException, IOException {
+        URL fcFile = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/featurecollection.json");
+        Path fcPath = Paths.get(fcFile.toURI());
+
+        JsonLocation streamLocation = null;
+        JsonLocation readerLocation = null;
+
+        //get Location from stream
+        try (InputStream stream = Files.newInputStream(fcPath);
+             JsonParser parser = GeoJSONParser.FACTORY.createParser(stream)) {
+            streamLocation = moveAndReturnPos(parser);
+        }
+
+        //get Location from reader
+        try (BufferedReader reader = Files.newBufferedReader(fcPath, Charset.forName("UTF-8"));
+             JsonParser parser = GeoJSONParser.FACTORY.createParser(reader)) {
+            readerLocation = moveAndReturnPos(parser);
+        }
+
+        Assert.assertFalse(streamLocation.equals(readerLocation));
+        Assert.assertFalse(GeoJSONUtils.equals(streamLocation, readerLocation));
+
+        LiteJsonLocation liteStreamLocation = new LiteJsonLocation(streamLocation);
+        LiteJsonLocation liteReaderLocation = new LiteJsonLocation(readerLocation);
+
+        Assert.assertTrue(liteStreamLocation.equals(liteReaderLocation));
+
+        Assert.assertTrue(liteStreamLocation.equals(streamLocation));
+        Assert.assertTrue(liteStreamLocation.equals(readerLocation));
+
+        Assert.assertTrue(liteReaderLocation.equals(streamLocation));
+        Assert.assertTrue(liteReaderLocation.equals(readerLocation));
+
+    }
+
+
+    @Test
+    public void testBefore () throws URISyntaxException, IOException {
+        URL fcFile = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/featurecollection.json");
+        Path fcPath = Paths.get(fcFile.toURI());
+
+        //get Location from stream
+        try (InputStream stream = Files.newInputStream(fcPath);
+             JsonParser parser = GeoJSONParser.FACTORY.createParser(stream)) {
+            parser.nextToken();
+
+            JsonLocation currentLocation = parser.getCurrentLocation();
+            LiteJsonLocation liteJsonLocation = new LiteJsonLocation(currentLocation);
+            Assert.assertFalse(liteJsonLocation.isBefore(currentLocation));
+
+            parser.nextToken();
+            currentLocation = parser.getCurrentLocation();
+            Assert.assertTrue(liteJsonLocation.isBefore(currentLocation));
+        }
+    }
+
+
+    private JsonLocation moveAndReturnPos(JsonParser parser) throws IOException {
+        parser.nextToken();
+        parser.nextToken();
+        parser.nextToken();
+        parser.nextToken();
+        return parser.getCurrentLocation();
+    }
+}
diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/test/suite/GeoJSONTestSuite.java b/storage/sis-geojson/src/test/java/org/apache/sis/test/suite/GeoJSONTestSuite.java
new file mode 100644
index 0000000..175eaee
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/test/suite/GeoJSONTestSuite.java
@@ -0,0 +1,34 @@
+/*
+ * 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.sis.test.suite;
+
+import org.apache.sis.test.TestSuite;
+import org.junit.runners.Suite;
+import org.junit.BeforeClass;
+
+
+/**
+ * All tests from the {@code sis-geojson} module, in rough dependency order.
+ */
+@Suite.SuiteClasses({
+    org.apache.sis.internal.storage.geojson.FeatureTypeUtilsTest.class,
+    org.apache.sis.internal.storage.geojson.GeoJSONReadTest.class,
+    org.apache.sis.internal.storage.geojson.GeoJSONWriteTest.class,
+    org.apache.sis.internal.storage.geojson.LiteJsonLocationTest.class
+})
+public final strictfp class GeoJSONTestSuite extends TestSuite {
+}
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/f_prop_array.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/f_prop_array.json
new file mode 100644
index 0000000..4ca0f62
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/f_prop_array.json
@@ -0,0 +1,7 @@
+{
+    "type":"FeatureCollection"
+    ,"crs":{"type":"name","properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}}
+    ,"features":[
+    {"type":"Feature","id":"id-0","geometry":{"type":"Point","coordinates":[-105.0162,39.5742]},"properties":{"array":[[0.0,1.0,2.0,3.0,4.0],[1.0,2.0,3.0,4.0,5.0],[2.0,3.0,4.0,5.0,6.0],[3.0,4.0,5.0,6.0,7.0],[4.0,5.0,6.0,7.0,8.0]]}}
+    ,{"type":"Feature","id":"id-1","geometry":{"type":"Point","coordinates":[-105.0162,39.5742]},"properties":{"array":[[0.0,-1.0,-2.0,-3.0,-4.0],[1.0,0.0,-1.0,-2.0,-3.0],[2.0,1.0,0.0,-1.0,-2.0],[3.0,2.0,1.0,0.0,-1.0],[4.0,3.0,2.0,1.0,0.0]]}}
+]}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/feature.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/feature.json
new file mode 100644
index 0000000..248f582
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/feature.json
@@ -0,0 +1,69 @@
+ {
+    "type": "Feature",
+    "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+            [
+                [
+                    -80.72487831115721,
+                    35.26545403190955
+                ],
+                [
+                    -80.72135925292969,
+                    35.26727607954368
+                ],
+                [
+                    -80.71517944335938,
+                    35.26769654625573
+                ],
+                [
+                    -80.7125186920166,
+                    35.27035945142482
+                ],
+                [
+                    -80.70857048034668,
+                    35.268257165144064
+                ],
+                [
+                    -80.70479393005371,
+                    35.268397319259996
+                ],
+                [
+                    -80.70324897766113,
+                    35.26503355355979
+                ],
+                [
+                    -80.71088790893555,
+                    35.2553619492954
+                ],
+                [
+                    -80.71681022644043,
+                    35.2553619492954
+                ],
+                [
+                    -80.7150936126709,
+                    35.26054831539319
+                ],
+                [
+                    -80.71869850158691,
+                    35.26026797976481
+                ],
+                [
+                    -80.72032928466797,
+                    35.26061839914875
+                ],
+                [
+                    -80.72264671325684,
+                    35.26033806376283
+                ],
+                [
+                    -80.72487831115721,
+                    35.26545403190955
+                ]
+            ]
+        ]
+    },
+    "properties": {
+        "name": "Plaza Road Park"
+    }
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/featurecollection.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/featurecollection.json
new file mode 100644
index 0000000..e4d7ae4
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/featurecollection.json
@@ -0,0 +1,158 @@
+{
+    "type": "FeatureCollection",
+    "features": [
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -80.87088507656375,
+                    35.21515162500578
+                ]
+            },
+            "properties": {
+                "name": "ABBOTT NEIGHBORHOOD PARK",
+                "address": "1300  SPRUCE ST"
+            }
+        },
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -80.83775386582222,
+                    35.24980190252168
+                ]
+            },
+            "properties": {
+                "name": "DOUBLE OAKS CENTER",
+                "address": "1326 WOODWARD AV"
+            }
+        },
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -80.83827000459532,
+                    35.25674709224663
+                ]
+            },
+            "properties": {
+                "name": "DOUBLE OAKS NEIGHBORHOOD PARK",
+                "address": "2605  DOUBLE OAKS RD"
+            }
+        },
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -80.83697759172735,
+                    35.25751734669229
+                ]
+            },
+            "properties": {
+                "name": "DOUBLE OAKS POOL",
+                "address": "1200 NEWLAND RD"
+            }
+        },
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -80.81647652154736,
+                    35.40148708491418
+                ]
+            },
+            "properties": {
+                "name": "DAVID B. WAYMER FLYING REGIONAL PARK",
+                "address": "15401 HOLBROOKS RD"
+            }
+        },
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -80.83556459443902,
+                    35.39917224760999
+                ]
+            },
+            "properties": {
+                "name": "DAVID B. WAYMER COMMUNITY PARK",
+                "address": "302 HOLBROOKS RD"
+            }
+        },
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Polygon",
+                "coordinates": [
+                    [
+                        [
+                            -80.72487831115721,
+                            35.26545403190955
+                        ],
+                        [
+                            -80.72135925292969,
+                            35.26727607954368
+                        ],
+                        [
+                            -80.71517944335938,
+                            35.26769654625573
+                        ],
+                        [
+                            -80.7125186920166,
+                            35.27035945142482
+                        ],
+                        [
+                            -80.70857048034668,
+                            35.268257165144064
+                        ],
+                        [
+                            -80.70479393005371,
+                            35.268397319259996
+                        ],
+                        [
+                            -80.70324897766113,
+                            35.26503355355979
+                        ],
+                        [
+                            -80.71088790893555,
+                            35.2553619492954
+                        ],
+                        [
+                            -80.71681022644043,
+                            35.2553619492954
+                        ],
+                        [
+                            -80.7150936126709,
+                            35.26054831539319
+                        ],
+                        [
+                            -80.71869850158691,
+                            35.26026797976481
+                        ],
+                        [
+                            -80.72032928466797,
+                            35.26061839914875
+                        ],
+                        [
+                            -80.72264671325684,
+                            35.26033806376283
+                        ],
+                        [
+                            -80.72487831115721,
+                            35.26545403190955
+                        ]
+                    ]
+                ]
+            },
+            "properties": {
+                "name": "Plaza Road Park"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometries.properties b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometries.properties
new file mode 100644
index 0000000..f638ee6
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometries.properties
@@ -0,0 +1,8 @@
+# WKT geometries for test purpose
+point=POINT(-105.01621 39.57422)
+multipoint=MULTIPOINT((-105.01621 39.57422),(-80.6665134 35.0539943))
+linestring=LINESTRING(-101.744384765625 39.32155002466662,-101.5521240234375 39.330048552942415,-101.40380859375 39.330048552942415,-101.33239746093749 39.364032338047984,-101.041259765625 39.36827914916011,-100.975341796875 39.30454987014581,-100.9149169921875 39.24501680713314,-100.843505859375 39.16414104768742,-100.8050537109375 39.104488809440475,-100.491943359375 39.10022600175347,-100.43701171875 39.095962936305476,-100.338134765625 39.095962936305476,-100.1953125 39.0277188402116 [...]
+multilinestring=MULTILINESTRING((-105.0214433670044 39.57805759162015,-105.02150774002075 39.57780951131517,-105.02157211303711 39.57749527498758,-105.02157211303711 39.57716449836683,-105.02157211303711 39.57703218727656,-105.02152919769287 39.57678410330158),(-105.01989841461182 39.574997872470774,-105.01959800720215 39.57489863607502,-105.01906156539916 39.57478286010041),(-105.01717329025269 39.5744024519653,-105.01698017120361 39.574385912433804,-105.0166368484497 39.574385912433804 [...]
+polygon=POLYGON((-84.32281494140625 34.9895035675793,-84.29122924804688 35.21981940793435,-84.24041748046875 35.25459097465022,-84.22531127929688 35.266925688950074,-84.20745849609375 35.26580442886754,-84.19921875 35.24674063355999,-84.16213989257812 35.24113278166642,-84.12368774414062 35.24898366572645,-84.09072875976562 35.24898366572645,-84.08798217773438 35.264683153268116,-84.04266357421875 35.27701633139884,-84.03030395507812 35.291589484566124,-84.0234375 35.306160014550784,-84. [...]
+multipolygon=MULTIPOLYGON(((-84.32281494140625 34.9895035675793,-84.29122924804688 35.21981940793435,-84.24041748046875 35.25459097465022,-84.22531127929688 35.266925688950074,-84.20745849609375 35.26580442886754,-84.19921875 35.24674063355999,-84.16213989257812 35.24113278166642,-84.12368774414062 35.24898366572645,-84.09072875976562 35.24898366572645,-84.08798217773438 35.264683153268116,-84.04266357421875 35.27701633139884,-84.03030395507812 35.291589484566124,-84.0234375 35.306160014 [...]
+geometrycollection=GEOMETRYCOLLECTION(POINT(-80.66080570220947 35.04939206472683),POLYGON((-80.66458225250244 35.04496519190309,-80.66344499588013 35.04603679820616,-80.66258668899536 35.045580049697556,-80.66387414932251 35.044280059194946,-80.66458225250244 35.04496519190309)),LINESTRING(-80.66237211227417 35.05950973022538,-80.66269397735596 35.0592638296087,-80.66284418106079 35.05893010615862,-80.66308021545409 35.05833291342246,-80.66359519958496 35.057753281001425,-80.663874149322 [...]
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometrycollection.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometrycollection.json
new file mode 100644
index 0000000..6287457
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometrycollection.json
@@ -0,0 +1,188 @@
+{
+    "type": "GeometryCollection",
+    "geometries": [
+        {
+            "type": "Point",
+            "coordinates": [
+                -80.66080570220947,
+                35.04939206472683
+            ]
+        },
+        {
+            "type": "Polygon",
+            "coordinates": [
+                [
+                    [
+                        -80.66458225250244,
+                        35.04496519190309
+                    ],
+                    [
+                        -80.66344499588013,
+                        35.04603679820616
+                    ],
+                    [
+                        -80.66258668899536,
+                        35.045580049697556
+                    ],
+                    [
+                        -80.66387414932251,
+                        35.044280059194946
+                    ],
+                    [
+                        -80.66458225250244,
+                        35.04496519190309
+                    ]
+                ]
+            ]
+        },
+        {
+            "type": "LineString",
+            "coordinates": [
+                [
+                    -80.66237211227417,
+                    35.05950973022538
+                ],
+                [
+                    -80.66269397735596,
+                    35.0592638296087
+                ],
+                [
+                    -80.66284418106079,
+                    35.05893010615862
+                ],
+                [
+                    -80.66308021545409,
+                    35.05833291342246
+                ],
+                [
+                    -80.66359519958496,
+                    35.057753281001425
+                ],
+                [
+                    -80.66387414932251,
+                    35.05740198662245
+                ],
+                [
+                    -80.66441059112549,
+                    35.05703312589789
+                ],
+                [
+                    -80.66486120223999,
+                    35.056787217822475
+                ],
+                [
+                    -80.66541910171509,
+                    35.05650617911516
+                ],
+                [
+                    -80.66563367843628,
+                    35.05631296444281
+                ],
+                [
+                    -80.66601991653441,
+                    35.055891403570705
+                ],
+                [
+                    -80.66619157791138,
+                    35.05545227534804
+                ],
+                [
+                    -80.66619157791138,
+                    35.05517123204622
+                ],
+                [
+                    -80.66625595092773,
+                    35.05489018777713
+                ],
+                [
+                    -80.6662130355835,
+                    35.054222703761525
+                ],
+                [
+                    -80.6662130355835,
+                    35.05392409072499
+                ],
+                [
+                    -80.66595554351807,
+                    35.05290528508858
+                ],
+                [
+                    -80.66569805145262,
+                    35.052044560077285
+                ],
+                [
+                    -80.66550493240356,
+                    35.0514824490509
+                ],
+                [
+                    -80.665762424469,
+                    35.05048117920187
+                ],
+                [
+                    -80.66617012023926,
+                    35.04972582715769
+                ],
+                [
+                    -80.66651344299316,
+                    35.049286665781096
+                ],
+                [
+                    -80.66692113876343,
+                    35.0485313026898
+                ],
+                [
+                    -80.66700696945189,
+                    35.048215102112344
+                ],
+                [
+                    -80.66707134246826,
+                    35.04777593261294
+                ],
+                [
+                    -80.66704988479614,
+                    35.04738946150025
+                ],
+                [
+                    -80.66696405410767,
+                    35.04698542156371
+                ],
+                [
+                    -80.66681385040283,
+                    35.046353007216055
+                ],
+                [
+                    -80.66659927368164,
+                    35.04596652937105
+                ],
+                [
+                    -80.66640615463257,
+                    35.04561518428889
+                ],
+                [
+                    -80.6659984588623,
+                    35.045193568195565
+                ],
+                [
+                    -80.66552639007568,
+                    35.044877354697526
+                ],
+                [
+                    -80.6649899482727,
+                    35.04454357245502
+                ],
+                [
+                    -80.66449642181396,
+                    35.04417465365292
+                ],
+                [
+                    -80.66385269165039,
+                    35.04387600387859
+                ],
+                [
+                    -80.66303730010986,
+                    35.043717894732545
+                ]
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/linestring.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/linestring.json
new file mode 100644
index 0000000..a7a82b8
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/linestring.json
@@ -0,0 +1,109 @@
+{
+    "type": "LineString",
+    "coordinates": [
+        [
+            -101.744384765625,
+            39.32155002466662
+        ],
+        [
+            -101.5521240234375,
+            39.330048552942415
+        ],
+        [
+            -101.40380859375,
+            39.330048552942415
+        ],
+        [
+            -101.33239746093749,
+            39.364032338047984
+        ],
+        [
+            -101.041259765625,
+            39.36827914916011
+        ],
+        [
+            -100.975341796875,
+            39.30454987014581
+        ],
+        [
+            -100.9149169921875,
+            39.24501680713314
+        ],
+        [
+            -100.843505859375,
+            39.16414104768742
+        ],
+        [
+            -100.8050537109375,
+            39.104488809440475
+        ],
+        [
+            -100.491943359375,
+            39.10022600175347
+        ],
+        [
+            -100.43701171875,
+            39.095962936305476
+        ],
+        [
+            -100.338134765625,
+            39.095962936305476
+        ],
+        [
+            -100.1953125,
+            39.027718840211605
+        ],
+        [
+            -100.008544921875,
+            39.01064750994083
+        ],
+        [
+            -99.86572265625,
+            39.00211029922512
+        ],
+        [
+            -99.6844482421875,
+            38.97222194853654
+        ],
+        [
+            -99.51416015625,
+            38.929502416386605
+        ],
+        [
+            -99.38232421875,
+            38.92095542046727
+        ],
+        [
+            -99.3218994140625,
+            38.89530825492018
+        ],
+        [
+            -99.1131591796875,
+            38.86965182408357
+        ],
+        [
+            -99.0802001953125,
+            38.85682013474361
+        ],
+        [
+            -98.82202148437499,
+            38.85682013474361
+        ],
+        [
+            -98.44848632812499,
+            38.84826438869913
+        ],
+        [
+            -98.20678710937499,
+            38.84826438869913
+        ],
+        [
+            -98.02001953125,
+            38.8782049970615
+        ],
+        [
+            -97.635498046875,
+            38.87392853923629
+        ]
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/longValue.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/longValue.json
new file mode 100644
index 0000000..40c662c
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/longValue.json
@@ -0,0 +1,24 @@
+
+{
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+            -105.01621,
+            39.57422
+        ]
+      },
+      "properties": {
+        "size": 853555090789
+      }
+    }],
+  "crs": {
+    "type": "name",
+    "properties": {
+      "name": "urn:ogc:def:crs:OGC:1.3:CRS84"
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multilinestring.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multilinestring.json
new file mode 100644
index 0000000..8f23c20
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multilinestring.json
@@ -0,0 +1,85 @@
+{
+    "type": "MultiLineString",
+    "coordinates": [
+        [
+            [
+                -105.0214433670044,
+                39.57805759162015
+            ],
+            [
+                -105.02150774002075,
+                39.57780951131517
+            ],
+            [
+                -105.02157211303711,
+                39.57749527498758
+            ],
+            [
+                -105.02157211303711,
+                39.57716449836683
+            ],
+            [
+                -105.02157211303711,
+                39.57703218727656
+            ],
+            [
+                -105.02152919769287,
+                39.57678410330158
+            ]
+        ],
+        [
+            [
+                -105.01989841461182,
+                39.574997872470774
+            ],
+            [
+                -105.01959800720215,
+                39.57489863607502
+            ],
+            [
+                -105.01906156539916,
+                39.57478286010041
+            ]
+        ],
+        [
+            [
+                -105.01717329025269,
+                39.5744024519653
+            ],
+            [
+                -105.01698017120361,
+                39.574385912433804
+            ],
+            [
+                -105.0166368484497,
+                39.574385912433804
+            ],
+            [
+                -105.01650810241699,
+                39.5744024519653
+            ],
+            [
+                -105.0159502029419,
+                39.574270135602866
+            ]
+        ],
+        [
+            [
+                -105.0142765045166,
+                39.57397242286402
+            ],
+            [
+                -105.01412630081175,
+                39.57403858136094
+            ],
+            [
+                -105.0138258934021,
+                39.57417089816531
+            ],
+            [
+                -105.01331090927124,
+                39.57445207053608
+            ]
+        ]
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipoint.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipoint.json
new file mode 100644
index 0000000..63a7143
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipoint.json
@@ -0,0 +1,13 @@
+{
+    "type": "MultiPoint",
+    "coordinates": [
+        [
+            -105.01621,
+            39.57422
+        ],
+        [
+            -80.6665134,
+            35.0539943
+        ]
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipolygon.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipolygon.json
new file mode 100644
index 0000000..f25f85b
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipolygon.json
@@ -0,0 +1,815 @@
+{
+    "type": "MultiPolygon",
+    "coordinates": [
+        [
+            [
+                [
+                    -84.32281494140625,
+                    34.9895035675793
+                ],
+                [
+                    -84.29122924804688,
+                    35.21981940793435
+                ],
+                [
+                    -84.24041748046875,
+                    35.25459097465022
+                ],
+                [
+                    -84.22531127929688,
+                    35.266925688950074
+                ],
+                [
+                    -84.20745849609375,
+                    35.26580442886754
+                ],
+                [
+                    -84.19921875,
+                    35.24674063355999
+                ],
+                [
+                    -84.16213989257812,
+                    35.24113278166642
+                ],
+                [
+                    -84.12368774414062,
+                    35.24898366572645
+                ],
+                [
+                    -84.09072875976562,
+                    35.24898366572645
+                ],
+                [
+                    -84.08798217773438,
+                    35.264683153268116
+                ],
+                [
+                    -84.04266357421875,
+                    35.27701633139884
+                ],
+                [
+                    -84.03030395507812,
+                    35.291589484566124
+                ],
+                [
+                    -84.0234375,
+                    35.306160014550784
+                ],
+                [
+                    -84.03305053710936,
+                    35.32745068492882
+                ],
+                [
+                    -84.03579711914062,
+                    35.34313496028189
+                ],
+                [
+                    -84.03579711914062,
+                    35.348735749472546
+                ],
+                [
+                    -84.01657104492188,
+                    35.35545618392078
+                ],
+                [
+                    -84.01107788085938,
+                    35.37337460834958
+                ],
+                [
+                    -84.00970458984374,
+                    35.39128905521763
+                ],
+                [
+                    -84.01931762695312,
+                    35.41479572901859
+                ],
+                [
+                    -84.00283813476562,
+                    35.429344044107154
+                ],
+                [
+                    -83.93692016601562,
+                    35.47409160773029
+                ],
+                [
+                    -83.91220092773438,
+                    35.47632833265728
+                ],
+                [
+                    -83.88885498046875,
+                    35.504282143299655
+                ],
+                [
+                    -83.88473510742186,
+                    35.516578738902936
+                ],
+                [
+                    -83.8751220703125,
+                    35.52104976129943
+                ],
+                [
+                    -83.85314941406249,
+                    35.52104976129943
+                ],
+                [
+                    -83.82843017578125,
+                    35.52104976129943
+                ],
+                [
+                    -83.8092041015625,
+                    35.53446133418443
+                ],
+                [
+                    -83.80233764648438,
+                    35.54116627999813
+                ],
+                [
+                    -83.76800537109374,
+                    35.56239491058853
+                ],
+                [
+                    -83.7432861328125,
+                    35.56239491058853
+                ],
+                [
+                    -83.71994018554688,
+                    35.56239491058853
+                ],
+                [
+                    -83.67050170898438,
+                    35.569097520776054
+                ],
+                [
+                    -83.6334228515625,
+                    35.570214567965984
+                ],
+                [
+                    -83.61007690429688,
+                    35.576916524038616
+                ],
+                [
+                    -83.59634399414061,
+                    35.574682600980914
+                ],
+                [
+                    -83.5894775390625,
+                    35.55904339525896
+                ],
+                [
+                    -83.55239868164062,
+                    35.56574628576276
+                ],
+                [
+                    -83.49746704101562,
+                    35.563512051219696
+                ],
+                [
+                    -83.47000122070312,
+                    35.586968406786475
+                ],
+                [
+                    -83.4466552734375,
+                    35.60818490437746
+                ],
+                [
+                    -83.37936401367188,
+                    35.63609277863135
+                ],
+                [
+                    -83.35739135742188,
+                    35.65618041632016
+                ],
+                [
+                    -83.32305908203124,
+                    35.66622234103479
+                ],
+                [
+                    -83.3148193359375,
+                    35.65394870599763
+                ],
+                [
+                    -83.29971313476561,
+                    35.660643649881614
+                ],
+                [
+                    -83.28598022460938,
+                    35.67180064238771
+                ],
+                [
+                    -83.26126098632811,
+                    35.6907639509368
+                ],
+                [
+                    -83.25714111328125,
+                    35.69968630125201
+                ],
+                [
+                    -83.25576782226562,
+                    35.715298012125295
+                ],
+                [
+                    -83.23516845703125,
+                    35.72310272092263
+                ],
+                [
+                    -83.19808959960936,
+                    35.72756221127198
+                ],
+                [
+                    -83.16238403320312,
+                    35.753199435570316
+                ],
+                [
+                    -83.15826416015625,
+                    35.76322914549896
+                ],
+                [
+                    -83.10333251953125,
+                    35.76991491635478
+                ],
+                [
+                    -83.08685302734375,
+                    35.7843988251953
+                ],
+                [
+                    -83.0511474609375,
+                    35.787740890986576
+                ],
+                [
+                    -83.01681518554688,
+                    35.78328477203738
+                ],
+                [
+                    -83.001708984375,
+                    35.77882840327371
+                ],
+                [
+                    -82.96737670898438,
+                    35.793310688351724
+                ],
+                [
+                    -82.94540405273438,
+                    35.820040281161
+                ],
+                [
+                    -82.9193115234375,
+                    35.85121343450061
+                ],
+                [
+                    -82.9083251953125,
+                    35.86902116501695
+                ],
+                [
+                    -82.90557861328125,
+                    35.87792352995116
+                ],
+                [
+                    -82.91244506835938,
+                    35.92353244718235
+                ],
+                [
+                    -82.88360595703125,
+                    35.94688293218141
+                ],
+                [
+                    -82.85614013671875,
+                    35.951329861522666
+                ],
+                [
+                    -82.8424072265625,
+                    35.94243575255426
+                ],
+                [
+                    -82.825927734375,
+                    35.92464453144099
+                ],
+                [
+                    -82.80670166015625,
+                    35.927980690382704
+                ],
+                [
+                    -82.80532836914062,
+                    35.94243575255426
+                ],
+                [
+                    -82.77923583984375,
+                    35.97356075349624
+                ],
+                [
+                    -82.78060913085938,
+                    35.99245209055831
+                ],
+                [
+                    -82.76138305664062,
+                    36.00356252895066
+                ],
+                [
+                    -82.69546508789062,
+                    36.04465753921525
+                ],
+                [
+                    -82.64465332031249,
+                    36.060201412392914
+                ],
+                [
+                    -82.61306762695312,
+                    36.060201412392914
+                ],
+                [
+                    -82.60620117187499,
+                    36.033552893400376
+                ],
+                [
+                    -82.60620117187499,
+                    35.991340960635405
+                ],
+                [
+                    -82.60620117187499,
+                    35.97911749857497
+                ],
+                [
+                    -82.5787353515625,
+                    35.96133453736691
+                ],
+                [
+                    -82.5677490234375,
+                    35.951329861522666
+                ],
+                [
+                    -82.53067016601562,
+                    35.97244935753683
+                ],
+                [
+                    -82.46475219726562,
+                    36.006895355244666
+                ],
+                [
+                    -82.41668701171875,
+                    36.070192281208456
+                ],
+                [
+                    -82.37960815429686,
+                    36.10126686921446
+                ],
+                [
+                    -82.35488891601562,
+                    36.117908916563685
+                ],
+                [
+                    -82.34115600585936,
+                    36.113471382052175
+                ],
+                [
+                    -82.29583740234375,
+                    36.13343831245866
+                ],
+                [
+                    -82.26287841796874,
+                    36.13565654678543
+                ],
+                [
+                    -82.23403930664062,
+                    36.13565654678543
+                ],
+                [
+                    -82.2216796875,
+                    36.154509006695
+                ],
+                [
+                    -82.20382690429688,
+                    36.15561783381855
+                ],
+                [
+                    -82.19009399414062,
+                    36.144528857027744
+                ],
+                [
+                    -82.15438842773438,
+                    36.15007354140755
+                ],
+                [
+                    -82.14065551757812,
+                    36.134547437460064
+                ],
+                [
+                    -82.1337890625,
+                    36.116799556445024
+                ],
+                [
+                    -82.12142944335938,
+                    36.10570509327921
+                ],
+                [
+                    -82.08984375,
+                    36.10792411128649
+                ],
+                [
+                    -82.05276489257811,
+                    36.12678323326429
+                ],
+                [
+                    -82.03628540039062,
+                    36.12900165569652
+                ],
+                [
+                    -81.91268920898438,
+                    36.29409768373033
+                ],
+                [
+                    -81.89071655273438,
+                    36.30959215409138
+                ],
+                [
+                    -81.86325073242188,
+                    36.33504067209607
+                ],
+                [
+                    -81.83029174804688,
+                    36.34499652561904
+                ],
+                [
+                    -81.80145263671875,
+                    36.35605709240176
+                ],
+                [
+                    -81.77947998046874,
+                    36.34610265300638
+                ],
+                [
+                    -81.76162719726562,
+                    36.33835943134047
+                ],
+                [
+                    -81.73690795898438,
+                    36.33835943134047
+                ],
+                [
+                    -81.71905517578125,
+                    36.33835943134047
+                ],
+                [
+                    -81.70669555664062,
+                    36.33504067209607
+                ],
+                [
+                    -81.70669555664062,
+                    36.342784223707234
+                ],
+                [
+                    -81.72317504882812,
+                    36.357163062654365
+                ],
+                [
+                    -81.73278808593749,
+                    36.379279167407965
+                ],
+                [
+                    -81.73690795898438,
+                    36.40028364332352
+                ],
+                [
+                    -81.73690795898438,
+                    36.41354670392876
+                ],
+                [
+                    -81.72454833984374,
+                    36.423492513472326
+                ],
+                [
+                    -81.71768188476562,
+                    36.445589751779174
+                ],
+                [
+                    -81.69845581054688,
+                    36.47541104282962
+                ],
+                [
+                    -81.69845581054688,
+                    36.51073994146672
+                ],
+                [
+                    -81.705322265625,
+                    36.53060536411363
+                ],
+                [
+                    -81.69158935546875,
+                    36.55929085774001
+                ],
+                [
+                    -81.68060302734375,
+                    36.56480607840351
+                ],
+                [
+                    -81.68197631835938,
+                    36.58686302344181
+                ],
+                [
+                    -81.04202270507812,
+                    36.56370306576917
+                ],
+                [
+                    -80.74264526367186,
+                    36.561496993252575
+                ],
+                [
+                    -79.89120483398438,
+                    36.54053616262899
+                ],
+                [
+                    -78.68408203124999,
+                    36.53943280355122
+                ],
+                [
+                    -77.88345336914062,
+                    36.54053616262899
+                ],
+                [
+                    -76.91665649414062,
+                    36.54163950596125
+                ],
+                [
+                    -76.91665649414062,
+                    36.55046568575947
+                ],
+                [
+                    -76.31103515625,
+                    36.551568887374
+                ],
+                [
+                    -75.79605102539062,
+                    36.54936246839778
+                ],
+                [
+                    -75.6298828125,
+                    36.07574221562703
+                ],
+                [
+                    -75.4925537109375,
+                    35.82226734114509
+                ],
+                [
+                    -75.3936767578125,
+                    35.639441068973916
+                ],
+                [
+                    -75.41015624999999,
+                    35.43829554739668
+                ],
+                [
+                    -75.43212890625,
+                    35.263561862152095
+                ],
+                [
+                    -75.487060546875,
+                    35.18727767598896
+                ],
+                [
+                    -75.5914306640625,
+                    35.17380831799959
+                ],
+                [
+                    -75.9210205078125,
+                    35.04798673426734
+                ],
+                [
+                    -76.17919921875,
+                    34.867904962568744
+                ],
+                [
+                    -76.41540527343749,
+                    34.62868797377061
+                ],
+                [
+                    -76.4593505859375,
+                    34.57442951865274
+                ],
+                [
+                    -76.53076171875,
+                    34.53371242139567
+                ],
+                [
+                    -76.5911865234375,
+                    34.551811369170494
+                ],
+                [
+                    -76.651611328125,
+                    34.615126683462194
+                ],
+                [
+                    -76.761474609375,
+                    34.63320791137959
+                ],
+                [
+                    -77.069091796875,
+                    34.59704151614417
+                ],
+                [
+                    -77.376708984375,
+                    34.45674800347809
+                ],
+                [
+                    -77.5909423828125,
+                    34.3207552752374
+                ],
+                [
+                    -77.8326416015625,
+                    33.97980872872457
+                ],
+                [
+                    -77.9150390625,
+                    33.80197351806589
+                ],
+                [
+                    -77.9754638671875,
+                    33.73804486328907
+                ],
+                [
+                    -78.11279296875,
+                    33.8521697014074
+                ],
+                [
+                    -78.2830810546875,
+                    33.8521697014074
+                ],
+                [
+                    -78.4808349609375,
+                    33.815666308702774
+                ],
+                [
+                    -79.6728515625,
+                    34.8047829195724
+                ],
+                [
+                    -80.782470703125,
+                    34.836349990763864
+                ],
+                [
+                    -80.782470703125,
+                    34.91746688928252
+                ],
+                [
+                    -80.9307861328125,
+                    35.092945313732635
+                ],
+                [
+                    -81.0516357421875,
+                    35.02999636902566
+                ],
+                [
+                    -81.0516357421875,
+                    35.05248370662468
+                ],
+                [
+                    -81.0516357421875,
+                    35.137879119634185
+                ],
+                [
+                    -82.3150634765625,
+                    35.19625600786368
+                ],
+                [
+                    -82.3590087890625,
+                    35.19625600786368
+                ],
+                [
+                    -82.40295410156249,
+                    35.22318504970181
+                ],
+                [
+                    -82.4688720703125,
+                    35.16931803601131
+                ],
+                [
+                    -82.6885986328125,
+                    35.1154153142536
+                ],
+                [
+                    -82.781982421875,
+                    35.06147690849717
+                ],
+                [
+                    -83.1060791015625,
+                    35.003003395276714
+                ],
+                [
+                    -83.616943359375,
+                    34.99850370014629
+                ],
+                [
+                    -84.05639648437499,
+                    34.985003130171066
+                ],
+                [
+                    -84.22119140625,
+                    34.985003130171066
+                ],
+                [
+                    -84.32281494140625,
+                    34.9895035675793
+                ]
+            ],
+            [
+                [
+                    -75.69030761718749,
+                    35.74205383068037
+                ],
+                [
+                    -75.5914306640625,
+                    35.74205383068037
+                ],
+                [
+                    -75.5419921875,
+                    35.585851593232356
+                ],
+                [
+                    -75.56396484375,
+                    35.32633026307483
+                ],
+                [
+                    -75.69030761718749,
+                    35.285984736065735
+                ],
+                [
+                    -75.970458984375,
+                    35.16482750605027
+                ],
+                [
+                    -76.2066650390625,
+                    34.994003757575776
+                ],
+                [
+                    -76.300048828125,
+                    35.02999636902566
+                ],
+                [
+                    -76.409912109375,
+                    35.07946034047981
+                ],
+                [
+                    -76.5252685546875,
+                    35.10642805736423
+                ],
+                [
+                    -76.4208984375,
+                    35.25907654252574
+                ],
+                [
+                    -76.3385009765625,
+                    35.294952147406576
+                ],
+                [
+                    -76.0858154296875,
+                    35.29943548054543
+                ],
+                [
+                    -75.948486328125,
+                    35.44277092585766
+                ],
+                [
+                    -75.8660888671875,
+                    35.53669637839501
+                ],
+                [
+                    -75.772705078125,
+                    35.567980458012094
+                ],
+                [
+                    -75.706787109375,
+                    35.634976650677295
+                ],
+                [
+                    -75.706787109375,
+                    35.74205383068037
+                ],
+                [
+                    -75.69030761718749,
+                    35.74205383068037
+                ]
+            ]
+        ],
+        [
+            [
+                [
+                    -109.0283203125,
+                    36.98500309285596
+                ],
+                [
+                    -109.0283203125,
+                    40.97989806962013
+                ],
+                [
+                    -102.06298828125,
+                    40.97989806962013
+                ],
+                [
+                    -102.06298828125,
+                    37.00255267215955
+                ],
+                [
+                    -109.0283203125,
+                    36.98500309285596
+                ]
+            ]
+        ]
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/point.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/point.json
new file mode 100644
index 0000000..9b8d23b
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/point.json
@@ -0,0 +1,7 @@
+{
+    "type": "Point",
+    "coordinates": [
+        -105.01621,
+        39.57422
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/polygon.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/polygon.json
new file mode 100644
index 0000000..77bc6fc
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/polygon.json
@@ -0,0 +1,789 @@
+{
+    "type": "Polygon",
+    "coordinates": [
+        [
+            [
+                -84.32281494140625,
+                34.9895035675793
+            ],
+            [
+                -84.29122924804688,
+                35.21981940793435
+            ],
+            [
+                -84.24041748046875,
+                35.25459097465022
+            ],
+            [
+                -84.22531127929688,
+                35.266925688950074
+            ],
+            [
+                -84.20745849609375,
+                35.26580442886754
+            ],
+            [
+                -84.19921875,
+                35.24674063355999
+            ],
+            [
+                -84.16213989257812,
+                35.24113278166642
+            ],
+            [
+                -84.12368774414062,
+                35.24898366572645
+            ],
+            [
+                -84.09072875976562,
+                35.24898366572645
+            ],
+            [
+                -84.08798217773438,
+                35.264683153268116
+            ],
+            [
+                -84.04266357421875,
+                35.27701633139884
+            ],
+            [
+                -84.03030395507812,
+                35.291589484566124
+            ],
+            [
+                -84.0234375,
+                35.306160014550784
+            ],
+            [
+                -84.03305053710936,
+                35.32745068492882
+            ],
+            [
+                -84.03579711914062,
+                35.34313496028189
+            ],
+            [
+                -84.03579711914062,
+                35.348735749472546
+            ],
+            [
+                -84.01657104492188,
+                35.35545618392078
+            ],
+            [
+                -84.01107788085938,
+                35.37337460834958
+            ],
+            [
+                -84.00970458984374,
+                35.39128905521763
+            ],
+            [
+                -84.01931762695312,
+                35.41479572901859
+            ],
+            [
+                -84.00283813476562,
+                35.429344044107154
+            ],
+            [
+                -83.93692016601562,
+                35.47409160773029
+            ],
+            [
+                -83.91220092773438,
+                35.47632833265728
+            ],
+            [
+                -83.88885498046875,
+                35.504282143299655
+            ],
+            [
+                -83.88473510742186,
+                35.516578738902936
+            ],
+            [
+                -83.8751220703125,
+                35.52104976129943
+            ],
+            [
+                -83.85314941406249,
+                35.52104976129943
+            ],
+            [
+                -83.82843017578125,
+                35.52104976129943
+            ],
+            [
+                -83.8092041015625,
+                35.53446133418443
+            ],
+            [
+                -83.80233764648438,
+                35.54116627999813
+            ],
+            [
+                -83.76800537109374,
+                35.56239491058853
+            ],
+            [
+                -83.7432861328125,
+                35.56239491058853
+            ],
+            [
+                -83.71994018554688,
+                35.56239491058853
+            ],
+            [
+                -83.67050170898438,
+                35.569097520776054
+            ],
+            [
+                -83.6334228515625,
+                35.570214567965984
+            ],
+            [
+                -83.61007690429688,
+                35.576916524038616
+            ],
+            [
+                -83.59634399414061,
+                35.574682600980914
+            ],
+            [
+                -83.5894775390625,
+                35.55904339525896
+            ],
+            [
+                -83.55239868164062,
+                35.56574628576276
+            ],
+            [
+                -83.49746704101562,
+                35.563512051219696
+            ],
+            [
+                -83.47000122070312,
+                35.586968406786475
+            ],
+            [
+                -83.4466552734375,
+                35.60818490437746
+            ],
+            [
+                -83.37936401367188,
+                35.63609277863135
+            ],
+            [
+                -83.35739135742188,
+                35.65618041632016
+            ],
+            [
+                -83.32305908203124,
+                35.66622234103479
+            ],
+            [
+                -83.3148193359375,
+                35.65394870599763
+            ],
+            [
+                -83.29971313476561,
+                35.660643649881614
+            ],
+            [
+                -83.28598022460938,
+                35.67180064238771
+            ],
+            [
+                -83.26126098632811,
+                35.6907639509368
+            ],
+            [
+                -83.25714111328125,
+                35.69968630125201
+            ],
+            [
+                -83.25576782226562,
+                35.715298012125295
+            ],
+            [
+                -83.23516845703125,
+                35.72310272092263
+            ],
+            [
+                -83.19808959960936,
+                35.72756221127198
+            ],
+            [
+                -83.16238403320312,
+                35.753199435570316
+            ],
+            [
+                -83.15826416015625,
+                35.76322914549896
+            ],
+            [
+                -83.10333251953125,
+                35.76991491635478
+            ],
+            [
+                -83.08685302734375,
+                35.7843988251953
+            ],
+            [
+                -83.0511474609375,
+                35.787740890986576
+            ],
+            [
+                -83.01681518554688,
+                35.78328477203738
+            ],
+            [
+                -83.001708984375,
+                35.77882840327371
+            ],
+            [
+                -82.96737670898438,
+                35.793310688351724
+            ],
+            [
+                -82.94540405273438,
+                35.820040281161
+            ],
+            [
+                -82.9193115234375,
+                35.85121343450061
+            ],
+            [
+                -82.9083251953125,
+                35.86902116501695
+            ],
+            [
+                -82.90557861328125,
+                35.87792352995116
+            ],
+            [
+                -82.91244506835938,
+                35.92353244718235
+            ],
+            [
+                -82.88360595703125,
+                35.94688293218141
+            ],
+            [
+                -82.85614013671875,
+                35.951329861522666
+            ],
+            [
+                -82.8424072265625,
+                35.94243575255426
+            ],
+            [
+                -82.825927734375,
+                35.92464453144099
+            ],
+            [
+                -82.80670166015625,
+                35.927980690382704
+            ],
+            [
+                -82.80532836914062,
+                35.94243575255426
+            ],
+            [
+                -82.77923583984375,
+                35.97356075349624
+            ],
+            [
+                -82.78060913085938,
+                35.99245209055831
+            ],
+            [
+                -82.76138305664062,
+                36.00356252895066
+            ],
+            [
+                -82.69546508789062,
+                36.04465753921525
+            ],
+            [
+                -82.64465332031249,
+                36.060201412392914
+            ],
+            [
+                -82.61306762695312,
+                36.060201412392914
+            ],
+            [
+                -82.60620117187499,
+                36.033552893400376
+            ],
+            [
+                -82.60620117187499,
+                35.991340960635405
+            ],
+            [
+                -82.60620117187499,
+                35.97911749857497
+            ],
+            [
+                -82.5787353515625,
+                35.96133453736691
+            ],
+            [
+                -82.5677490234375,
+                35.951329861522666
+            ],
+            [
+                -82.53067016601562,
+                35.97244935753683
+            ],
+            [
+                -82.46475219726562,
+                36.006895355244666
+            ],
+            [
+                -82.41668701171875,
+                36.070192281208456
+            ],
+            [
+                -82.37960815429686,
+                36.10126686921446
+            ],
+            [
+                -82.35488891601562,
+                36.117908916563685
+            ],
+            [
+                -82.34115600585936,
+                36.113471382052175
+            ],
+            [
+                -82.29583740234375,
+                36.13343831245866
+            ],
+            [
+                -82.26287841796874,
+                36.13565654678543
+            ],
+            [
+                -82.23403930664062,
+                36.13565654678543
+            ],
+            [
+                -82.2216796875,
+                36.154509006695
+            ],
+            [
+                -82.20382690429688,
+                36.15561783381855
+            ],
+            [
+                -82.19009399414062,
+                36.144528857027744
+            ],
+            [
+                -82.15438842773438,
+                36.15007354140755
+            ],
+            [
+                -82.14065551757812,
+                36.134547437460064
+            ],
+            [
+                -82.1337890625,
+                36.116799556445024
+            ],
+            [
+                -82.12142944335938,
+                36.10570509327921
+            ],
+            [
+                -82.08984375,
+                36.10792411128649
+            ],
+            [
+                -82.05276489257811,
+                36.12678323326429
+            ],
+            [
+                -82.03628540039062,
+                36.12900165569652
+            ],
+            [
+                -81.91268920898438,
+                36.29409768373033
+            ],
+            [
+                -81.89071655273438,
+                36.30959215409138
+            ],
+            [
+                -81.86325073242188,
+                36.33504067209607
+            ],
+            [
+                -81.83029174804688,
+                36.34499652561904
+            ],
+            [
+                -81.80145263671875,
+                36.35605709240176
+            ],
+            [
+                -81.77947998046874,
+                36.34610265300638
+            ],
+            [
+                -81.76162719726562,
+                36.33835943134047
+            ],
+            [
+                -81.73690795898438,
+                36.33835943134047
+            ],
+            [
+                -81.71905517578125,
+                36.33835943134047
+            ],
+            [
+                -81.70669555664062,
+                36.33504067209607
+            ],
+            [
+                -81.70669555664062,
+                36.342784223707234
+            ],
+            [
+                -81.72317504882812,
+                36.357163062654365
+            ],
+            [
+                -81.73278808593749,
+                36.379279167407965
+            ],
+            [
+                -81.73690795898438,
+                36.40028364332352
+            ],
+            [
+                -81.73690795898438,
+                36.41354670392876
+            ],
+            [
+                -81.72454833984374,
+                36.423492513472326
+            ],
+            [
+                -81.71768188476562,
+                36.445589751779174
+            ],
+            [
+                -81.69845581054688,
+                36.47541104282962
+            ],
+            [
+                -81.69845581054688,
+                36.51073994146672
+            ],
+            [
+                -81.705322265625,
+                36.53060536411363
+            ],
+            [
+                -81.69158935546875,
+                36.55929085774001
+            ],
+            [
+                -81.68060302734375,
+                36.56480607840351
+            ],
+            [
+                -81.68197631835938,
+                36.58686302344181
+            ],
+            [
+                -81.04202270507812,
+                36.56370306576917
+            ],
+            [
+                -80.74264526367186,
+                36.561496993252575
+            ],
+            [
+                -79.89120483398438,
+                36.54053616262899
+            ],
+            [
+                -78.68408203124999,
+                36.53943280355122
+            ],
+            [
+                -77.88345336914062,
+                36.54053616262899
+            ],
+            [
+                -76.91665649414062,
+                36.54163950596125
+            ],
+            [
+                -76.91665649414062,
+                36.55046568575947
+            ],
+            [
+                -76.31103515625,
+                36.551568887374
+            ],
+            [
+                -75.79605102539062,
+                36.54936246839778
+            ],
+            [
+                -75.6298828125,
+                36.07574221562703
+            ],
+            [
+                -75.4925537109375,
+                35.82226734114509
+            ],
+            [
+                -75.3936767578125,
+                35.639441068973916
+            ],
+            [
+                -75.41015624999999,
+                35.43829554739668
+            ],
+            [
+                -75.43212890625,
+                35.263561862152095
+            ],
+            [
+                -75.487060546875,
+                35.18727767598896
+            ],
+            [
+                -75.5914306640625,
+                35.17380831799959
+            ],
+            [
+                -75.9210205078125,
+                35.04798673426734
+            ],
+            [
+                -76.17919921875,
+                34.867904962568744
+            ],
+            [
+                -76.41540527343749,
+                34.62868797377061
+            ],
+            [
+                -76.4593505859375,
+                34.57442951865274
+            ],
+            [
+                -76.53076171875,
+                34.53371242139567
+            ],
+            [
+                -76.5911865234375,
+                34.551811369170494
+            ],
+            [
+                -76.651611328125,
+                34.615126683462194
+            ],
+            [
+                -76.761474609375,
+                34.63320791137959
+            ],
+            [
+                -77.069091796875,
+                34.59704151614417
+            ],
+            [
+                -77.376708984375,
+                34.45674800347809
+            ],
+            [
+                -77.5909423828125,
+                34.3207552752374
+            ],
+            [
+                -77.8326416015625,
+                33.97980872872457
+            ],
+            [
+                -77.9150390625,
+                33.80197351806589
+            ],
+            [
+                -77.9754638671875,
+                33.73804486328907
+            ],
+            [
+                -78.11279296875,
+                33.8521697014074
+            ],
+            [
+                -78.2830810546875,
+                33.8521697014074
+            ],
+            [
+                -78.4808349609375,
+                33.815666308702774
+            ],
+            [
+                -79.6728515625,
+                34.8047829195724
+            ],
+            [
+                -80.782470703125,
+                34.836349990763864
+            ],
+            [
+                -80.782470703125,
+                34.91746688928252
+            ],
+            [
+                -80.9307861328125,
+                35.092945313732635
+            ],
+            [
+                -81.0516357421875,
+                35.02999636902566
+            ],
+            [
+                -81.0516357421875,
+                35.05248370662468
+            ],
+            [
+                -81.0516357421875,
+                35.137879119634185
+            ],
+            [
+                -82.3150634765625,
+                35.19625600786368
+            ],
+            [
+                -82.3590087890625,
+                35.19625600786368
+            ],
+            [
+                -82.40295410156249,
+                35.22318504970181
+            ],
+            [
+                -82.4688720703125,
+                35.16931803601131
+            ],
+            [
+                -82.6885986328125,
+                35.1154153142536
+            ],
+            [
+                -82.781982421875,
+                35.06147690849717
+            ],
+            [
+                -83.1060791015625,
+                35.003003395276714
+            ],
+            [
+                -83.616943359375,
+                34.99850370014629
+            ],
+            [
+                -84.05639648437499,
+                34.985003130171066
+            ],
+            [
+                -84.22119140625,
+                34.985003130171066
+            ],
+            [
+                -84.32281494140625,
+                34.9895035675793
+            ]
+        ],
+        [
+            [
+                -75.69030761718749,
+                35.74205383068037
+            ],
+            [
+                -75.5914306640625,
+                35.74205383068037
+            ],
+            [
+                -75.5419921875,
+                35.585851593232356
+            ],
+            [
+                -75.56396484375,
+                35.32633026307483
+            ],
+            [
+                -75.69030761718749,
+                35.285984736065735
+            ],
+            [
+                -75.970458984375,
+                35.16482750605027
+            ],
+            [
+                -76.2066650390625,
+                34.994003757575776
+            ],
+            [
+                -76.300048828125,
+                35.02999636902566
+            ],
+            [
+                -76.409912109375,
+                35.07946034047981
+            ],
+            [
+                -76.5252685546875,
+                35.10642805736423
+            ],
+            [
+                -76.4208984375,
+                35.25907654252574
+            ],
+            [
+                -76.3385009765625,
+                35.294952147406576
+            ],
+            [
+                -76.0858154296875,
+                35.29943548054543
+            ],
+            [
+                -75.948486328125,
+                35.44277092585766
+            ],
+            [
+                -75.8660888671875,
+                35.53669637839501
+            ],
+            [
+                -75.772705078125,
+                35.567980458012094
+            ],
+            [
+                -75.706787109375,
+                35.634976650677295
+            ],
+            [
+                -75.706787109375,
+                35.74205383068037
+            ],
+            [
+                -75.69030761718749,
+                35.74205383068037
+            ]
+        ]
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/sample_with_null_properties.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/sample_with_null_properties.json
new file mode 100644
index 0000000..720099b
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/sample_with_null_properties.json
@@ -0,0 +1,502 @@
+{
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.834892363946428,
+              43.48256740738211
+            ],
+            [
+              3.8171876558029396,
+              43.47857177599835
+            ],
+            [
+              3.6412946044113017,
+              43.473329643558024
+            ],
+            [
+              3.6589021586871184,
+              43.52508450712277
+            ],
+            [
+              3.8279642042032593,
+              43.52668001576773
+            ],
+            [
+              3.834892363946428,
+              43.48256740738211
+            ]
+          ]
+        ]
+      },
+      "properties": null
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.6946333003797647,
+              43.454643754862055
+            ],
+            [
+              3.7292860446964538,
+              43.40531143854098
+            ],
+            [
+              3.636426622539704,
+              43.43205503941921
+            ],
+            [
+              3.6946333003797647,
+              43.454643754862055
+            ]
+          ]
+        ]
+      },
+      "properties": null
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823533
+            ],
+            [
+              3.6300962842853837,
+              43.371516200704754
+            ],
+            [
+              3.5565671428569434,
+              43.31364096674475
+            ],
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823533
+            ],
+            [
+              3.6300962842853837,
+              43.371516200704754
+            ],
+            [
+              3.5565671428569434,
+              43.31364096674475
+            ],
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784403,
+              43.31621752144202
+            ],
+            [
+              3.6238508748053055,
+              43.375682458235445
+            ],
+            [
+              3.6300962842853837,
+              43.37151620070485
+            ],
+            [
+              3.556567142856943,
+              43.31364096674483
+            ],
+            [
+              3.5462226673784403,
+              43.31621752144202
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.633385150301334,
+              43.38457565109916
+            ],
+            [
+              3.64118828280889,
+              43.37902289968621
+            ],
+            [
+              3.654474857633081,
+              43.38559747028079
+            ],
+            [
+              3.6479331503130545,
+              43.391373120329625
+            ],
+            [
+              3.633385150301334,
+              43.38457565109916
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Parking visiteurs"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.546222667378441,
+              43.31621752144182
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823523
+            ],
+            [
+              3.6300962842853846,
+              43.37151620070465
+            ],
+            [
+              3.556567142856944,
+              43.31364096674463
+            ],
+            [
+              3.546222667378441,
+              43.31621752144182
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784416,
+              43.31621752144171
+            ],
+            [
+              3.6238508748053064,
+              43.37568245823512
+            ],
+            [
+              3.6300962842853846,
+              43.371516200704534
+            ],
+            [
+              3.5565671428569443,
+              43.31364096674452
+            ],
+            [
+              3.5462226673784416,
+              43.31621752144171
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.546222667378441,
+              43.31621752144182
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823523
+            ],
+            [
+              3.6300962842853846,
+              43.37151620070465
+            ],
+            [
+              3.556567142856944,
+              43.31364096674463
+            ],
+            [
+              3.546222667378441,
+              43.31621752144182
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.546222667378441,
+              43.31621752144182
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823523
+            ],
+            [
+              3.6300962842853846,
+              43.37151620070465
+            ],
+            [
+              3.556567142856944,
+              43.31364096674463
+            ],
+            [
+              3.546222667378441,
+              43.31621752144182
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784416,
+              43.31621752144171
+            ],
+            [
+              3.6238508748053064,
+              43.37568245823512
+            ],
+            [
+              3.6300962842853846,
+              43.371516200704534
+            ],
+            [
+              3.5565671428569443,
+              43.31364096674452
+            ],
+            [
+              3.5462226673784416,
+              43.31621752144171
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.546222667378441,
+              43.31621752144182
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823523
+            ],
+            [
+              3.6300962842853846,
+              43.37151620070465
+            ],
+            [
+              3.556567142856944,
+              43.31364096674463
+            ],
+            [
+              3.546222667378441,
+              43.31621752144182
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823533
+            ],
+            [
+              3.6300962842853837,
+              43.371516200704754
+            ],
+            [
+              3.5565671428569434,
+              43.31364096674475
+            ],
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823533
+            ],
+            [
+              3.6300962842853837,
+              43.371516200704754
+            ],
+            [
+              3.5565671428569434,
+              43.31364096674475
+            ],
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.546222667378441,
+              43.31621752144182
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823523
+            ],
+            [
+              3.6300962842853846,
+              43.37151620070465
+            ],
+            [
+              3.556567142856944,
+              43.31364096674463
+            ],
+            [
+              3.546222667378441,
+              43.31621752144182
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    }
+  ],
+  "crs": {
+    "type": "name",
+    "properties": {
+      "name": "urn:ogc:def:crs:OGC:1.3:CRS84"
+    }
+  }
+}