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 2023/05/12 15:09:24 UTC

[sis] branch feat/coverage-json created (now 48c170932f)

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

jsorel pushed a change to branch feat/coverage-json
in repository https://gitbox.apache.org/repos/asf/sis.git


      at 48c170932f feat(coveragejson): initial binding and grid coverage implementation of CoverageJson

This branch includes the following new commits:

     new 48c170932f feat(coveragejson): initial binding and grid coverage implementation of CoverageJson

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[sis] 01/01: feat(coveragejson): initial binding and grid coverage implementation of CoverageJson

Posted by js...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 48c170932fed91e60c6c7d02529dbd72dd5b00f5
Author: jsorel <jo...@geomatys.com>
AuthorDate: Fri May 12 17:08:22 2023 +0200

    feat(coveragejson): initial binding and grid coverage implementation of CoverageJson
---
 storage/pom.xml                                    |   1 +
 storage/{ => sis-coveragejson}/pom.xml             | 153 +++----
 .../internal/coveragejson/CoverageJsonStore.java   | 128 ++++++
 .../coveragejson/CoverageJsonStoreProvider.java    |  99 +++++
 .../internal/coveragejson/CoverageResource.java    | 457 +++++++++++++++++++++
 .../sis/internal/coveragejson/binding/Axe.java     | 136 ++++++
 .../sis/internal/coveragejson/binding/Axes.java    |  58 +++
 .../internal/coveragejson/binding/Category.java    |  68 +++
 .../coveragejson/binding/CategoryEncoding.java     |  31 ++
 .../internal/coveragejson/binding/Coverage.java    | 131 ++++++
 .../coveragejson/binding/CoverageCollection.java   |  90 ++++
 .../coveragejson/binding/CoverageJsonObject.java   |  55 +++
 .../internal/coveragejson/binding/Dictionary.java  |  69 ++++
 .../sis/internal/coveragejson/binding/Domain.java  |  90 ++++
 .../coveragejson/binding/GeographicCRS.java        |  72 ++++
 .../sis/internal/coveragejson/binding/I18N.java    | 119 ++++++
 .../coveragejson/binding/IdentifierRS.java         |  84 ++++
 .../internal/coveragejson/binding/Identifiers.java |  29 ++
 .../sis/internal/coveragejson/binding/NdArray.java | 104 +++++
 .../coveragejson/binding/ObservedProperty.java     |  88 ++++
 .../internal/coveragejson/binding/Parameter.java   |  91 ++++
 .../coveragejson/binding/ParameterGroup.java       |  86 ++++
 .../internal/coveragejson/binding/Parameters.java  |  71 ++++
 .../coveragejson/binding/ProjectedCRS.java         |  64 +++
 .../sis/internal/coveragejson/binding/Ranges.java  |  67 +++
 .../binding/ReferenceSystemConnection.java         |  67 +++
 .../sis/internal/coveragejson/binding/Symbol.java  |  60 +++
 .../coveragejson/binding/TargetConcept.java        |  54 +++
 .../internal/coveragejson/binding/TemporalRS.java  |  79 ++++
 .../sis/internal/coveragejson/binding/TileSet.java |  75 ++++
 .../coveragejson/binding/TiledNdArray.java         |  79 ++++
 .../sis/internal/coveragejson/binding/Unit.java    |  79 ++++
 .../internal/coveragejson/binding/VerticalCRS.java |  61 +++
 .../org.apache.sis.storage.DataStoreProvider       |   1 +
 .../coveragejson/CoverageJsonStoreTest.java        |  86 ++++
 .../coveragejson/CoverageJsonTestSuite.java        |  44 ++
 .../internal/coveragejson/binding/BindingTest.java | 234 +++++++++++
 .../internal/coveragejson/binding/axe_bounds.json  |  12 +
 .../internal/coveragejson/binding/axe_polygon.json |  33 ++
 .../internal/coveragejson/binding/axe_regular.json |   5 +
 .../internal/coveragejson/binding/axe_tuples.json  |  20 +
 .../binding/coverage_vertical_profile.json         |  91 ++++
 .../binding/coverage_vertical_profile_nocs.json    |  80 ++++
 .../coveragejson/binding/coveragecollection.json   |  92 +++++
 .../internal/coveragejson/binding/domain_grid.json |  26 ++
 .../coveragejson/binding/domain_trajectory.json    |  27 ++
 .../coveragejson/binding/domaintype_grid.json      |   5 +
 .../binding/domaintype_multipoint.json             |  30 ++
 .../binding/domaintype_multipointseries.json       |  31 ++
 .../binding/domaintype_multipolygon.json           |  32 ++
 .../binding/domaintype_multipolygonseries.json     |  32 ++
 .../coveragejson/binding/domaintype_point.json     |  24 ++
 .../binding/domaintype_pointseries.json            |  26 ++
 .../coveragejson/binding/domaintype_polygon.json   |  29 ++
 .../binding/domaintype_polygonseries.json          |  31 ++
 .../coveragejson/binding/domaintype_section.json   |  31 ++
 .../binding/domaintype_trajectory.json             |  30 ++
 .../binding/domaintype_vertical_profile.json       |  26 ++
 .../binding/geographiccrs_longlat.json             |   4 +
 .../binding/geographiccrs_longlatheight.json       |   4 +
 .../sis/internal/coveragejson/binding/ndarray.json |  10 +
 .../binding/parameter_categoricaldata.json         |  33 ++
 .../binding/parameter_continuousdata.json          |  24 ++
 .../binding/parametergroup_uncertainty.json        |  13 +
 .../binding/parametergroup_vectorquantity.json     |   9 +
 .../binding/projectedcrs_britishnationalgrid.json  |   4 +
 .../binding/reference_system_connection.json       |   7 +
 .../internal/coveragejson/binding/temporalrs.json  |   4 +
 .../coveragejson/binding/tiledndarray.json         |  16 +
 .../coveragejson/binding/verticalcrs_navd88.json   |   4 +
 .../sis/internal/coveragejson/coverage_xyzt.json   |  58 +++
 71 files changed, 4177 insertions(+), 86 deletions(-)

diff --git a/storage/pom.xml b/storage/pom.xml
index 4287379b06..b44712ecf0 100644
--- a/storage/pom.xml
+++ b/storage/pom.xml
@@ -175,6 +175,7 @@
     <module>sis-netcdf</module>
     <module>sis-geotiff</module>
     <module>sis-earth-observation</module>
+    <module>sis-coveragejson</module>
   </modules>
 
 </project>
diff --git a/storage/pom.xml b/storage/sis-coveragejson/pom.xml
similarity index 51%
copy from storage/pom.xml
copy to storage/sis-coveragejson/pom.xml
index 4287379b06..7ae7a10ba8 100644
--- a/storage/pom.xml
+++ b/storage/sis-coveragejson/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-coveragejson</artifactId>
+  <name>Apache SIS Coverage-JSON storage</name>
   <description>
-    Group of modules for reading and writing data from/to various storages.
-    Storages are typically file formats or a database schemas.
+    DataStore for Coverage-JSON format.
   </description>
 
 
@@ -59,40 +58,7 @@
         <role>developer</role>
       </roles>
     </developer>
-    <developer>
-      <name>Martin Desruisseaux</name>
-      <id>desruisseaux</id>
-      <email>desruisseaux@apache.org</email>
-      <organization>Geomatys</organization>
-      <organizationUrl>https://www.geomatys.com</organizationUrl>
-      <timezone>+1</timezone>
-      <roles>
-        <role>developer</role>
-      </roles>
-    </developer>
   </developers>
-  <contributors>
-    <contributor>
-      <name>Thi Phuong Hao Nguyen</name>
-      <email>nguyenthiphuonghao243@gmail.com</email>
-      <organization>VNSC</organization>
-      <organizationUrl>https://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>https://vnsc.org.vn</organizationUrl>
-      <timezone>+7</timezone>
-      <roles>
-        <role>developer</role>
-      </roles>
-    </contributor>
-  </contributors>
 
 
   <!-- ===========================================================
@@ -100,21 +66,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.coveragejson
+              </Automatic-Module-Name>
+            </manifestEntries>
+          </archive>
+        </configuration>
       </plugin>
     </plugins>
   </build>
@@ -125,56 +88,74 @@
        =========================================================== -->
   <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.storage</groupId>
+      <artifactId>sis-storage</artifactId>
       <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.sis.core</groupId>
       <artifactId>sis-referencing</artifactId>
       <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
     </dependency>
+    
+    <!--dependency>
+        <groupId>jakarta.json</groupId>
+        <artifactId>jakarta.json-api</artifactId>
+        <version>2.1.1</version>
+    </dependency>
+    <dependency>
+        <groupId>jakarta.json.bind</groupId>
+        <artifactId>jakarta.json.bind-api</artifactId>
+        <version>3.0.0</version>
+    </dependency-->
+    
+    <!-- this dependencis pulls jakarta jsonb-api -->
     <dependency>
-      <groupId>org.opengis</groupId>
-      <artifactId>geoapi-pending</artifactId>
+        <groupId>org.eclipse</groupId>
+        <artifactId>yasson</artifactId>
+        <version>3.0.3</version>
     </dependency>
+    
+    <!--dependency>
+        <groupId>javax.json.bind</groupId>
+        <artifactId>javax.json.bind-api</artifactId>
+        <version>1.0</version>
+    </dependency--> 
+    <!--dependency>
+        <groupId>org.apache.johnzon</groupId>
+        <artifactId>johnzon-core</artifactId>
+        <version>1.2.19</version>
+    </dependency>    
 
-    <!-- Test dependencies -->
     <dependency>
-      <groupId>org.opengis</groupId>
-      <artifactId>geoapi-conformance</artifactId>
+      <groupId>org.apache.geronimo.specs</groupId>
+      <artifactId>geronimo-json_1.1_spec</artifactId>
+      <version>1.5</version>
     </dependency>
     <dependency>
-      <groupId>org.apache.derby</groupId>
-      <artifactId>derby</artifactId>
-      <scope>test</scope>
+        <groupId>org.apache.johnzon</groupId>
+        <artifactId>johnzon-mapper</artifactId>
+        <version>1.2.19</version>
     </dependency>
     <dependency>
-      <groupId>org.apache.derby</groupId>
-      <artifactId>derbytools</artifactId>
-      <scope>test</scope>
+        <groupId>org.apache.johnzon</groupId>
+        <artifactId>johnzon-jsonb</artifactId>
+        <version>1.2.19</version>
     </dependency>
     <dependency>
-      <groupId>org.apache.sis.core</groupId>
-      <artifactId>sis-utility</artifactId>
-      <version>${project.version}</version>
-      <type>test-jar</type>
-      <scope>test</scope>
-    </dependency>
+        <groupId>org.apache.johnzon</groupId>
+        <artifactId>johnzon-jsonb-extras</artifactId>
+        <version>1.2.19</version>
+    </dependency-->
   </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>
-  </modules>
-
 </project>
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStore.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStore.java
new file mode 100644
index 0000000000..ac1800821f
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStore.java
@@ -0,0 +1,128 @@
+/*
+ * 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.coveragejson;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import org.apache.sis.internal.coveragejson.binding.Coverage;
+import org.apache.sis.internal.coveragejson.binding.CoverageCollection;
+import org.apache.sis.internal.coveragejson.binding.CoverageJsonObject;
+import org.apache.sis.internal.storage.MetadataBuilder;
+import org.apache.sis.internal.storage.URIDataStore;
+import org.apache.sis.internal.storage.io.IOUtilities;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.WritableAggregate;
+import org.opengis.metadata.Metadata;
+import org.opengis.parameter.ParameterValueGroup;
+
+/**
+ * A data store backed by Coverage-JSON files.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class CoverageJsonStore extends DataStore implements WritableAggregate {
+
+    /**
+     * The {@link CoverageJsonStoreProvider#LOCATION} parameter value, or {@code null} if none.
+     * This is used for information purpose only, not for actual reading operations.
+     *
+     * @see #getOpenParameters()
+     */
+    private final URI location;
+
+    /**
+     * Same value than {@link #location} but as a path, or {@code null} if none.
+     * Stored separately because conversion from path to URI back to path is not
+     * looseness (relative paths become absolutes).
+     */
+    private final Path path;
+
+    private boolean parsed = false;
+    private final List<Resource> components = new ArrayList<>();
+
+    CoverageJsonStore(CoverageJsonStoreProvider provider, StorageConnector connector) throws DataStoreException {
+        super(provider, connector);
+        location = connector.getStorageAs(URI.class);
+        path = connector.getStorageAs(Path.class);
+    }
+
+    @Override
+    public Optional<ParameterValueGroup> getOpenParameters() {
+        return Optional.ofNullable(URIDataStore.parameters(provider, location));
+    }
+
+    @Override
+    public Metadata getMetadata() throws DataStoreException {
+        final MetadataBuilder builder = new MetadataBuilder();
+        builder.addIdentifier(null, IOUtilities.filename(path), MetadataBuilder.Scope.ALL);
+        return builder.buildAndFreeze();
+    }
+
+    @Override
+    public synchronized Collection<? extends Resource> components() throws DataStoreException {
+        if (!parsed) {
+            parsed = true;
+            if (Files.exists(path)) {
+                try (final Jsonb b = JsonbBuilder.create();
+                     final InputStream in = new BufferedInputStream(Files.newInputStream(path))) {
+                    final CoverageJsonObject obj = b.fromJson(in, CoverageJsonObject.class);
+
+                    if (obj instanceof Coverage) {
+                        final Coverage coverage = (Coverage) obj;
+                        components.add(new CoverageResource(this, coverage));
+
+                    } else if (obj instanceof CoverageCollection) {
+                        throw new UnsupportedOperationException("Coverage collection not supported yet.");
+                    }
+
+                } catch (Exception ex) {
+                    throw new DataStoreException("Failed to parse coverage json object.", ex);
+                }
+            }
+        }
+
+        return Collections.unmodifiableList(components);
+    }
+
+
+    @Override
+    public Resource add(Resource resource) throws DataStoreException {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public void remove(Resource resource) throws DataStoreException {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public void close() throws DataStoreException {
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreProvider.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreProvider.java
new file mode 100644
index 0000000000..b9c48eb7f0
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreProvider.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coveragejson;
+
+import java.net.URI;
+import java.util.logging.Logger;
+import org.apache.sis.internal.storage.Capability;
+import org.apache.sis.internal.storage.StoreMetadata;
+import org.apache.sis.internal.storage.URIDataStore;
+import org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreProvider;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.ProbeResult;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.util.Version;
+import org.opengis.parameter.ParameterDescriptorGroup;
+
+/**
+ * The provider of {@link CoverageJsonStore} instances. Given a {@link StorageConnector} input,
+ * this class tries to instantiate a {@code CoverageJsonStore}.
+ *
+ * Draft specification : https://github.com/opengeospatial/CoverageJSON
+ *
+ * @author Johann Sorel (Geomatys)
+ *
+ * @see CoverageJsonStore
+ */
+@StoreMetadata(formatName    = CoverageJsonStoreProvider.NAME,
+               fileSuffixes  = {"covjson"},
+               capabilities  = Capability.READ,
+               resourceTypes = {Aggregate.class, GridCoverageResource.class})
+public class CoverageJsonStoreProvider extends DataStoreProvider {
+
+    public static final String NAME = "CoverageJSON";
+    /**
+     * The MIME type for Coverage-JSON files.
+     */
+    private static final String MIME_TYPE = "application/vnd.cov+json";
+
+    /**
+     * The logger used by Coverage-JSON stores.
+     *
+     * @see #getLogger()
+     */
+    private static final Logger LOGGER = Logger.getLogger("org.apache.sis.internal.coveragejson");
+
+    /**
+     * The parameter descriptor to be returned by {@link #getOpenParameters()}.
+     */
+    private static final ParameterDescriptorGroup OPEN_DESCRIPTOR = URIDataStore.Provider.descriptor(NAME);
+
+    @Override
+    public String getShortName() {
+        return NAME;
+    }
+
+    @Override
+    public ParameterDescriptorGroup getOpenParameters() {
+        return OPEN_DESCRIPTOR;
+    }
+
+    @Override
+    public ProbeResult probeContent(StorageConnector connector) throws DataStoreException {
+        final URI uri = connector.getStorageAs(URI.class);
+        if (uri != null && uri.toString().toLowerCase().endsWith(".covjson")) {
+            return new ProbeResult(true, MIME_TYPE, new Version("0.9"));
+        }
+        return ProbeResult.UNSUPPORTED_STORAGE;
+    }
+
+    @Override
+    public DataStore open(StorageConnector connector) throws DataStoreException {
+        return new CoverageJsonStore(this, connector);
+    }
+
+    /**
+     * {@return the logger used by CoverageJSON stores}.
+     */
+    @Override
+    public Logger getLogger() {
+        return LOGGER;
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageResource.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageResource.java
new file mode 100644
index 0000000000..50f7e9d56c
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageResource.java
@@ -0,0 +1,457 @@
+/*
+ * 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.coveragejson;
+
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferDouble;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.DateTimeParseException;
+import java.time.format.SignStyle;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.measure.Unit;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.BufferedGridCoverage;
+import org.apache.sis.coverage.grid.DisjointExtentException;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridRoundingMode;
+import org.apache.sis.internal.coveragejson.binding.Axe;
+import org.apache.sis.internal.coveragejson.binding.Axes;
+import org.apache.sis.internal.coveragejson.binding.Coverage;
+import org.apache.sis.internal.coveragejson.binding.CoverageJsonObject;
+import org.apache.sis.internal.coveragejson.binding.Domain;
+import org.apache.sis.internal.coveragejson.binding.GeographicCRS;
+import org.apache.sis.internal.coveragejson.binding.IdentifierRS;
+import org.apache.sis.internal.coveragejson.binding.NdArray;
+import org.apache.sis.internal.coveragejson.binding.Parameter;
+import org.apache.sis.internal.coveragejson.binding.ProjectedCRS;
+import org.apache.sis.internal.coveragejson.binding.ReferenceSystemConnection;
+import org.apache.sis.internal.coveragejson.binding.TemporalRS;
+import org.apache.sis.internal.coveragejson.binding.VerticalCRS;
+import org.apache.sis.measure.Units;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.storage.AbstractGridCoverageResource;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.NoSuchDataException;
+import org.opengis.metadata.spatial.DimensionNameType;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransform1D;
+import org.opengis.util.FactoryException;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+final class CoverageResource extends AbstractGridCoverageResource {
+
+    private static final DateTimeFormatter YEAR = new DateTimeFormatterBuilder()
+                .appendValue(ChronoField.YEAR, 1, 19, SignStyle.EXCEEDS_PAD)
+                .toFormatter();
+
+
+    private static final DateTimeFormatter YEAR_MONTH = new DateTimeFormatterBuilder()
+                .appendValue(ChronoField.YEAR, 1, 19, SignStyle.EXCEEDS_PAD)
+                .appendLiteral('-')
+                .appendValue(ChronoField.MONTH_OF_YEAR, 1)
+                .toFormatter();
+
+    private static final DateTimeFormatter YEAR_MONTH_DAY = new DateTimeFormatterBuilder()
+                .appendValue(ChronoField.YEAR, 1, 19, SignStyle.EXCEEDS_PAD)
+                .appendLiteral('-')
+                .appendValue(ChronoField.MONTH_OF_YEAR, 1)
+                .appendLiteral('-')
+                .appendValue(ChronoField.DAY_OF_MONTH, 1)
+                .toFormatter();
+
+    private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ISO_DATE_TIME;
+
+    private final CoverageJsonStore store;
+    private final Coverage binding;
+
+    private final GridGeometry gridGeometry;
+    private final List<SampleDimension> sampleDimensions;
+    private final Map<String,double[]> datas;
+
+    public CoverageResource(CoverageJsonStore store, Coverage binding) throws DataStoreException {
+        super(null);
+        this.store = store;
+        this.binding = binding;
+
+        //rebuild grid geometry
+        try {
+            gridGeometry = jsonToGridGeometry(binding.domain);
+        } catch (FactoryException ex) {
+            throw new DataStoreException("Failed to create GridGeometry from JSON Domain", ex);
+        }
+        //rebuild sample dimensions
+        sampleDimensions = new ArrayList<>();
+        for (Entry<String,Parameter> entry : binding.parameters.any.entrySet()) {
+            final SampleDimension sd = jsonToSampleDimension(entry.getKey(), entry.getValue());
+            sampleDimensions.add(sd);
+        }
+        if (binding.parameterGroups != null) {
+            throw new UnsupportedOperationException("Parameter groups not supported yet.");
+        }
+        //read datas
+        datas = new HashMap<>();
+        for (Entry<String,NdArray> entry : binding.ranges.any.entrySet()) {
+            datas.put(entry.getKey(), jsonToDataBuffer(entry.getValue()));
+        }
+    }
+
+    @Override
+    public GridGeometry getGridGeometry() throws DataStoreException {
+        return gridGeometry;
+    }
+
+    @Override
+    public List<SampleDimension> getSampleDimensions() throws DataStoreException {
+        return Collections.unmodifiableList(sampleDimensions);
+    }
+
+    @Override
+    public GridCoverage read(GridGeometry domain, int... ranges) throws DataStoreException {
+
+        final GridGeometry intersection;
+        if (domain != null) {
+            try {
+                intersection = gridGeometry.derive().rounding(GridRoundingMode.ENCLOSING).subgrid(domain).build();
+            } catch (DisjointExtentException ex) {
+                throw new NoSuchDataException(ex);
+            }
+        } else {
+            intersection = gridGeometry;
+        }
+
+        final double[][] rawDatas;
+        final List<SampleDimension> selected;
+        if (ranges == null || ranges.length == 0) {
+            selected = sampleDimensions;
+            rawDatas = new double[sampleDimensions.size()][0];
+            for (int i = 0; i < rawDatas.length; i++) {
+                rawDatas[i] = datas.get(sampleDimensions.get(i).getName().toString());
+            }
+
+        } else {
+            selected = new ArrayList<>();
+            rawDatas = new double[ranges.length][0];
+            for (int i = 0; i < rawDatas.length; i++) {
+                final SampleDimension sd = sampleDimensions.get(ranges[i]);
+                selected.add(sd);
+                rawDatas[i] = datas.get(sd.getName().toString());
+            }
+        }
+
+        final DataBuffer buffer = new DataBufferDouble(rawDatas, rawDatas[0].length);
+        return new BufferedGridCoverage(intersection, selected, buffer);
+    }
+
+    /**
+     * Transform JSON domain to GridGeometry.
+     */
+    private static GridGeometry jsonToGridGeometry(Domain domain) throws DataStoreException, FactoryException {
+
+        if (Domain.DOMAINTYPE_GRID.equalsIgnoreCase(domain.domainType)) {
+
+            //build coordinate system
+            final List<ReferenceSystemConnection> referencing = domain.referencing;
+            final List<String> axeNames = new ArrayList<>();
+            final List<CoordinateReferenceSystem> crss = new ArrayList<>();
+            if (referencing != null && !referencing.isEmpty()) {
+                for (ReferenceSystemConnection rsc : referencing) {
+                    axeNames.addAll(rsc.coordinates);
+                    final CoordinateReferenceSystem crs = jsonToCoordinateReferenceSystem(rsc.system);
+                    if (crs.getCoordinateSystem().getDimension() != rsc.coordinates.size()) {
+                        throw new DataStoreException("Declared CRS " + rsc.system.toString() + " do not match coordinates length");
+                    }
+                    crss.add(crs);
+                }
+            } else {
+                throw new DataStoreException("Coverage domain must be defined, Coverage as part of CoverageCollection not supported yet.");
+            }
+
+            //build extent
+            final int dimension = axeNames.size();
+            final Axes axes = domain.axes;
+            final GridGeometry[] axeGrids = new GridGeometry[dimension];
+
+            //check if axes declared on crs are ordered in the same way as the grid extent.
+            final int[] reorder = new int[dimension];
+            boolean inOrder = true;
+
+            for (int i = 0; i < dimension; i++) {
+                final String axeName = axeNames.get(i);
+                final Axe axe;
+                final int realIdx;
+                switch (axeName) {
+                    case "x" :
+                        if (axes.x == null) throw new DataStoreException("X axe is undefined");
+                        axe = axes.x;
+                        realIdx = 0;
+                        reorder[i] = realIdx;
+                        inOrder &= (i == realIdx);
+                        break;
+                    case "y" :
+                        if (axes.y == null) throw new DataStoreException("Y axe is undefined");
+                        axe = axes.y;
+                        realIdx = 1;
+                        reorder[i] = realIdx;
+                        inOrder &= (i == realIdx);
+                        break;
+                    case "z" :
+                        if (axes.z == null) throw new DataStoreException("Z axe is undefined");
+                        axe = axes.z;
+                        realIdx = 2;
+                        reorder[i] = realIdx;
+                        inOrder &= (i == realIdx);
+                        break;
+                    case "t" :
+                        if (axes.t == null) throw new DataStoreException("T axe is undefined");
+                        axe = axes.t;
+                        realIdx = reorder.length == 3 ? 2 : 3;
+                        reorder[i] = realIdx;
+                        inOrder &= (i == realIdx);
+                        break;
+                    default: throw new DataStoreException("Unexpected axe name :" + axeName);
+                }
+                axeGrids[realIdx] = jsonAxeToGridGeometry(axeName, axe);
+            }
+
+            final DimensionNameType[] dnt = new DimensionNameType[dimension];
+            final long[] lower = new long[dimension];
+            final long[] upper = new long[dimension]; //inclusive
+            MathTransform gridToCrs = null;
+            for (int i = 0; i < dimension; i++) {
+                dnt[i] = axeGrids[i].getExtent().getAxisType(0).get();
+                upper[i] = axeGrids[i].getExtent().getHigh(0);
+                if (gridToCrs == null) {
+                    gridToCrs = axeGrids[i].getGridToCRS(PixelInCell.CELL_CENTER);
+                } else {
+                    gridToCrs = MathTransforms.compound(gridToCrs, axeGrids[i].getGridToCRS(PixelInCell.CELL_CENTER));
+                }
+            }
+
+
+            if (!inOrder) {
+                final MatrixSIS m = Matrices.createZero(dimension+1, dimension+1);
+                for (int i = 0; i < dimension; i++) {
+                    m.setElement(i, reorder[i], 1.0);
+                }
+                m.setElement(dimension, dimension, 1.0);
+                final MathTransform reorderTrs = MathTransforms.linear(m);
+                gridToCrs = MathTransforms.concatenate(reorderTrs, gridToCrs);
+            }
+
+
+            final CoordinateReferenceSystem crs = CRS.compound(crss.toArray(CoordinateReferenceSystem[]::new));
+            final GridExtent extent = new GridExtent(dnt, lower, upper, true);
+            return new GridGeometry(extent, PixelInCell.CELL_CENTER, gridToCrs, crs);
+
+        } else {
+            throw new DataStoreException("Unsupported domain type " + domain.domainType);
+        }
+    }
+
+    /**
+     * Transform JSON axe to 1D GridGeometry without CRS.
+     */
+    private static GridGeometry jsonAxeToGridGeometry(String axeName, Axe axe) throws DataStoreException {
+
+        if (axe.dataType == null || Axe.DATATYPE_PRIMITIVE.equals(axe.dataType)) {
+
+        } else if (Axe.DATATYPE_TUPLE.equals(axe.dataType) ) {
+            throw new UnsupportedOperationException("Tuple axe data type not supported yet.");
+        } else if (Axe.DATATYPE_POLYGON.equals(axe.dataType) ) {
+            throw new UnsupportedOperationException("Polygon axe data type not supported yet.");
+        } else {
+            throw new DataStoreException("Unexpected axe data type :" + axe.dataType);
+        }
+
+        //rebuild axe transform
+        final MathTransform1D axeTrs;
+        final int size;
+        if (axe.values != null) {
+            final double[] values = new double[axe.values.size()];
+            for (int i = 0; i < values.length; i++) {
+                values[i] = asDouble(axe.values.get(i));
+            }
+            size = values.length;
+            axeTrs = MathTransforms.interpolate(null, values);
+        } else if (axe.start != null) {
+            size = axe.num;
+            if (axe.num == 1) {
+                axeTrs = (MathTransform1D) MathTransforms.linear(1.0, axe.start);
+            } else {
+                final double step = (axe.stop - axe.start) / (axe.num -1);
+                axeTrs = (MathTransform1D) MathTransforms.linear(step, axe.start);
+            }
+        } else {
+            throw new DataStoreException("Axe must have values or star/stop values");
+        }
+
+        final GridExtent extent = new GridExtent(new DimensionNameType[]{DimensionNameType.valueOf(axeName)}, new long[]{0}, new long[]{size-1}, true);
+        return new GridGeometry(extent, PixelInCell.CELL_CENTER, axeTrs, null);
+    }
+
+    /**
+     * Transform JSON system object to CoordinateReferenceSystem.
+     */
+    private static CoordinateReferenceSystem jsonToCoordinateReferenceSystem(CoverageJsonObject obj) throws FactoryException {
+        if (obj instanceof GeographicCRS) {
+            final GeographicCRS jcrs = (GeographicCRS) obj;
+            if (jcrs.id != null) {
+                if (jcrs.id.equals("http://www.opengis.net/def/crs/EPSG/0/4979")) {
+                    return CommonCRS.WGS84.geographic3D();
+                }
+                return CRS.forCode(jcrs.id);
+            } else {
+                throw new UnsupportedOperationException("Geographic CRS wihout id not supported");
+            }
+        } else if (obj instanceof ProjectedCRS) {
+            final ProjectedCRS jcrs = (ProjectedCRS) obj;
+            throw new UnsupportedOperationException("ProjectedCRS not supported yet");
+
+        } else if (obj instanceof VerticalCRS) {
+            final VerticalCRS jcrs = (VerticalCRS) obj;
+            throw new UnsupportedOperationException("VerticalCRS not supported yet");
+
+        } else if (obj instanceof TemporalRS) {
+            final TemporalRS jcrs = (TemporalRS) obj;
+            if (jcrs.timeScale != null) {
+                throw new UnsupportedOperationException("TemporalRS timeScale not supported yet");
+            }
+            if ("Gregorian".equalsIgnoreCase(jcrs.calendar)) {
+                return CommonCRS.Temporal.JAVA.crs();
+            } else {
+                throw new UnsupportedOperationException(jcrs.calendar + "calendar not supported yet");
+            }
+
+        } else if (obj instanceof IdentifierRS) {
+            final IdentifierRS jcrs = (IdentifierRS) obj;
+            throw new UnsupportedOperationException("IdentifierRS not supported yet");
+
+        } else {
+            throw new UnsupportedOperationException("Unsupported system " + String.valueOf(obj));
+        }
+    }
+
+    /**
+     * Transform JSON parameter to SampleDimension.
+     */
+    private static SampleDimension jsonToSampleDimension(String name, Parameter parameter) {
+        final SampleDimension.Builder builder = new SampleDimension.Builder();
+
+        builder.setName(name);
+
+//        if (parameter.id != null) {
+//            builder.setName(parameter.id);
+//        } else if (parameter.label != null) {
+//            builder.setName(parameter.label);
+//        } else if (parameter.description != null) {
+//            builder.setName(parameter.description);
+//        }
+
+        Unit unit = jsonToUnit(parameter.unit);
+
+        //TODO categories
+        //parameter.categoryEncoding;
+        //parameter.observedProperty;
+
+        return builder.build();
+    }
+
+    /**
+     * Transform JSON unit to SIS Unit.
+     */
+    private static Unit jsonToUnit(org.apache.sis.internal.coveragejson.binding.Unit unit) {
+        if (unit == null) return Units.UNITY;
+
+        if (unit.symbol instanceof String) {
+            return Units.valueOf(unit.symbol.toString());
+        }
+        return Units.UNITY;
+    }
+
+    /**
+     * Transform JSON NdArray to number array.
+     */
+    private static double[] jsonToDataBuffer(NdArray array) throws DataStoreException {
+        //TODO more work on checking axes order
+        double[] values = new double[array.values.size()];
+        for (int i = 0; i < values.length; i++) {
+            values[i] = asDouble(array.values.get(i));
+        }
+
+        return values;
+    }
+
+    private static double asDouble(Object cdt) throws DataStoreException {
+        if (cdt == null) {
+            return Double.NaN;
+        } else if (cdt instanceof String) {
+            final Instant instant = parseDataTime(String.valueOf(cdt));
+            return instant.toEpochMilli();
+        } else if (cdt instanceof Number) {
+            return ((Number) cdt).doubleValue();
+        } else {
+            throw new DataStoreException("Unexpected value : " + cdt);
+        }
+    }
+
+    /**
+     * If the calendar is based on years, months, days, then the referenced
+     * values SHOULD use one of the following ISO8601-based lexical representations:
+     * YYYY
+     * ±XYYYY (where X stands for extra year digits)
+     * YYYY-MM
+     * YYYY-MM-DD
+     * YYYY-MM-DDTHH:MM:SS[.F]Z where Z is either “Z” or a time scale offset +|-HH:MM
+     *
+     * If calendar dates with reduced precision are used in a lexical
+     * representation (e.g. "2016"), then a client SHOULD interpret those dates
+     * in that reduced precision.
+     */
+    private static Instant parseDataTime(String str) throws DataStoreException {
+
+        for (DateTimeFormatter dtf : Arrays.asList(YEAR,YEAR_MONTH, YEAR_MONTH_DAY, DATE_TIME)) {
+            try {
+                TemporalAccessor accesser = dtf.parse(str);
+                return Instant.from(accesser);
+            } catch (DateTimeParseException ex) {
+                //do nothing
+            }
+        }
+        throw new DataStoreException("Unable to parse date : " + str);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Axe.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Axe.java
new file mode 100644
index 0000000000..1e8533c3b7
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Axe.java
@@ -0,0 +1,136 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * An axis object MUST have either a "values" member or, as a compact notation
+ * for a regularly spaced numeric axis, have all the members "start", "stop",
+ * and "num".
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"start","stop","num","dataType","coordinates","values","bounds"})
+public final class Axe extends Dictionary<Object> {
+
+    public static final String DATATYPE_PRIMITIVE = "primitive";
+    public static final String DATATYPE_TUPLE = "tuple";
+    public static final String DATATYPE_POLYGON = "polygon";
+
+    /**
+     * The values of "start" and "stop" MUST be numbers
+     */
+    public Double start;
+    /**
+     * The values of "start" and "stop" MUST be numbers
+     */
+    public Double stop;
+    /**
+     * the value of "num" an integer greater than zero.
+     *
+     * If the value of "num" is 1, then "start" and "stop" MUST have identical
+     * values. For num > 1, the array elements of "values" MAY be reconstructed
+     * with the formula start + i * step where i is the ith element and in the
+     * interval [0, num-1] and step = (stop - start) / (num - 1).
+     *
+     * If num = 1 then "values" is [start]. Note that "start" can be greater
+     * than "stop" in which case the axis values are descending.
+     */
+    public Integer num;
+    /**
+     * The value of "dataType" determines the structure of an axis value and its
+     * coordinates that are made available for referencing. The values of
+     * "dataType" defined in this Community Standard are "primitive", "tuple",
+     * and "polygon". Custom values MAY be used as detailed in the Extensions
+     * section. For "primitive", there is a single coordinate identifier and
+     * each axis value MUST be a number or string. For "tuple", each axis value
+     * MUST be an array of fixed size of primitive values in a defined order,
+     * where the tuple size corresponds to the number of coordinate identifiers.
+     * For "polygon", each axis value MUST be a GeoJSON Polygon coordinate
+     * array, where the order of coordinates is given by the "coordinates"
+     * array.
+     *
+     * If missing, the member "dataType" defaults to "primitive" and MUST not be
+     * included for that default case.
+     *
+     * If "dataType" is "primitive" and the associated reference system (see
+     * 6.1.2) defines a natural ordering of values then the array values in
+     * "values", if existing, MUST be ordered monotonically, that is, increasing
+     * or decreasing.
+     */
+    public String dataType;
+    /**
+     * The value of "coordinates" is a non-empty array of coordinate identifiers
+     * corresponding to the order of the coordinates defined by "dataType".
+     *
+     * If missing, the member "coordinates" defaults to a one-element array of
+     * the axis identifier and MUST NOT be included for that default case.
+     *
+     * A coordinate identifier SHALL NOT be defined more than once in all axis
+     * objects of a domain object.
+     */
+    public List<String> coordinates;
+    /**
+     * The value of "values" is a non-empty array of axis values.
+     */
+    public List<Object> values;
+    /**
+     * An axis object MAY have axis value bounds defined in the member "bounds"
+     * where the value is an array of values of length len*2 with len being the
+     * length of the "values" array. For each axis value at array index i in the
+     * "values" array, a lower and upper bounding value at positions 2*i and
+     * 2*i+1, respectively, are given in the bounds array.
+     *
+     * If a domain axis object has no "bounds" member then a bounds array MAY be
+     * derived automatically.
+     */
+    public List<Object> bounds;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Axe)) return false;
+
+        final Axe cdt = ((Axe) other);
+        return super.equals(other)
+            && Objects.equals(start, cdt.start)
+            && Objects.equals(stop, cdt.stop)
+            && Objects.equals(num, cdt.num)
+            && Objects.equals(dataType, cdt.dataType)
+            && Objects.equals(coordinates, cdt.coordinates)
+            && Objects.equals(values, cdt.values)
+            && Objects.equals(bounds, cdt.bounds);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                start,
+                stop,
+                num,
+                dataType,
+                coordinates,
+                values,
+                bounds);
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Axes.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Axes.java
new file mode 100644
index 0000000000..1f34d2c25c
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Axes.java
@@ -0,0 +1,58 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * The "axes" member MUST NOT be empty.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"x","y","z","t"})
+public final class Axes extends Dictionary<Object> {
+
+    public Axe x;
+    public Axe y;
+    public Axe z;
+    public Axe t;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Axes)) return false;
+
+        final Axes cdt = ((Axes) other);
+        return super.equals(other)
+            && Objects.equals(x, cdt.x)
+            && Objects.equals(y, cdt.y)
+            && Objects.equals(y, cdt.y)
+            && Objects.equals(t, cdt.t);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                x,
+                y,
+                z,
+                t);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Category.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Category.java
new file mode 100644
index 0000000000..cd2e7d14b3
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Category.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.internal.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * A category object MUST an "id" and a "label" member, and MAY have a
+ * "description" member.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"id","label","description"})
+public final class Category extends Dictionary<Object> {
+
+    /**
+     * The value of "id" MUST be a string and SHOULD be a common identifier.
+     */
+    public String id;
+    /**
+     * The value of "label" MUST be an i18n object of the name of the
+     * category and SHOULD be short.
+     */
+    public I18N label;
+    /**
+     * If given, the value of "description" MUST be an i18n object with a
+     * textual description of the* category.
+     */
+    public I18N description;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Category)) return false;
+
+        final Category cdt = ((Category) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(description, cdt.description);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                label,
+                description);
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CategoryEncoding.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CategoryEncoding.java
new file mode 100644
index 0000000000..94cf4a3cc0
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CategoryEncoding.java
@@ -0,0 +1,31 @@
+/*
+ * 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.coveragejson.binding;
+
+
+/**
+ * CategoryEncoding is an object where each key is equal to an "id" value of
+ * the "categories" array within the "observedProperty" member of the
+ * parameter object. There MUST be no duplicate keys. The value is either
+ * an integer or an array of integers where each integer MUST be unique
+ * within the object.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class CategoryEncoding extends Dictionary<String> {
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Coverage.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Coverage.java
new file mode 100644
index 0000000000..db43a0cc02
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Coverage.java
@@ -0,0 +1,131 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.JsonbException;
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import jakarta.json.bind.serializer.DeserializationContext;
+import jakarta.json.bind.serializer.JsonbDeserializer;
+import jakarta.json.stream.JsonParser;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A CoverageJSON object with the type "Coverage" is a coverage object.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","domain","parameters","parameterGroups","ranges"})
+public final class Coverage extends CoverageJsonObject {
+
+    /**
+     * If a coverage has a commonly used identifier, that identifier SHOULD be
+     * included as a member of the coverage object with the name "id".
+     */
+    public String id;
+    /**
+     * A coverage object MUST have a member with the name "domain" where the
+     * value is either a domain object or a URL.
+     *
+     * If the value of "domain" is a URL and the referenced domain has a
+     * "domainType" member, then the coverage object SHOULD have the member
+     * "domainType" where the value MUST equal that of the referenced domain.
+     *
+     * If the coverage object is part of a coverage collection which has a
+     * "domainType" member then that member SHOULD be omitted in the coverage
+     * object.
+     */
+    //@JsonbTypeDeserializer(Coverage.DomainDeserializer.class)
+    //TODO should be a Domain or an URL, DomainDeserializer not working as expected
+    public Domain domain;
+    /**
+     * A coverage object MAY have a member with the name "parameters" where the
+     * value is an object where each member has as name a short identifier and
+     * as value a parameter object. The identifier corresponds to the commonly
+     * known concept of “variable name” and is merely used in clients for
+     * conveniently accessing the corresponding range object.
+     *
+     * A coverage object MUST have a "parameters" member if the coverage object
+     * is not part of a coverage collection or if the coverage collection does
+     * not have a "parameters" member.
+     */
+    public Parameters parameters;
+    /**
+     * A coverage object MAY have a member with the name "parameterGroups" where
+     * the value is an array of ParameterGroup objects.
+     */
+    public List<ParameterGroup> parameterGroups;
+    /**
+     * A coverage object MUST have a member with the name "ranges" where the
+     * value is a range set object. Any member of a range set object has as
+     * name any of the names in a "parameters" object in scope and as value
+     * either an NdArray or TiledNdArray object or a URL resolving to a
+     * CoverageJSON document of such object. A "parameters" member in scope is
+     * either within the enclosing coverage object or, if part of a coverage
+     * collection, in the parent coverage collection object. The shape and axis
+     * names of each NdArray or TiledNdArray object MUST correspond to the
+     * domain axes defined by "domain", while single-valued axes MAY be omitted.
+     * If the referenced parameter object has a "categoryEncoding" member, then
+     * each non-null array element of the "values" member of the NdArray object,
+     * or the linked NdArray objects within a TiledNdArray object, MUST be equal
+     * to one of the values defined in the "categoryEncoding" object and be
+     * interpreted as the matching category.
+     */
+    public Ranges ranges;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Coverage)) return false;
+
+        final Coverage cdt = ((Coverage) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(domain, cdt.domain)
+            && Objects.equals(parameters, cdt.parameters)
+            && Objects.equals(parameterGroups, cdt.parameterGroups)
+            && Objects.equals(ranges, cdt.ranges);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                domain,
+                parameters,
+                parameterGroups,
+                ranges);
+    }
+
+    public static class DomainDeserializer implements JsonbDeserializer<Object> {
+        @Override
+        public Object deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
+            final JsonParser.Event event = parser.next();
+            if (event == JsonParser.Event.START_OBJECT) {
+                // Deserialize inner object
+                return ctx.deserialize(Domain.class, parser);
+            } else if (event == JsonParser.Event.VALUE_STRING) {
+                return parser.getString();
+            } else {
+                throw new JsonbException("Unexpected json element");
+            }
+        }
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CoverageCollection.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CoverageCollection.java
new file mode 100644
index 0000000000..6e80503edd
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CoverageCollection.java
@@ -0,0 +1,90 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A CoverageJSON object with the type "CoverageCollection" is a coverage collection object.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","domainType","parameters","parameterGroups","referencing","coverages"})
+public final class CoverageCollection extends CoverageJsonObject {
+
+    /**
+     * A coverage collection object MAY have the member "domainType" with a
+     * string value to indicate that the coverage collection only contains
+     * coverages of the given domain type. See the section Common Domain Types
+     * for details. Custom domain types may be used as recommended in the
+     * section Extensions.
+     *
+     * If a coverage collection object has the member "domainType", then this
+     * member is inherited to all included coverages.
+     */
+    public String domainType;
+    /**
+     * A coverage collection object MAY have a member with the name "parameters"
+     * where the value is an object where each member has as name a short
+     * identifier and as value a parameter object.
+     */
+    public Parameter parameters;
+    /**
+     * A coverage collection object MAY have a member with the name
+     * "parameterGroups" where the value is an array of ParameterGroup objects.
+     */
+    public List<ParameterGroup> parameterGroups;
+    /**
+     * A coverage collection object MAY have a member with the name "referencing"
+     * where the value is an array of reference system connection objects.
+     */
+    public List<ReferenceSystemConnection> referencing;
+    /**
+     * A coverage collection object MUST have a member with the name "coverages".
+     * The value corresponding to "coverages" is an array. Each element in the
+     * array is a coverage object as defined above.
+     */
+    public List<Coverage> coverages;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof CoverageCollection)) return false;
+
+        final CoverageCollection cdt = ((CoverageCollection) other);
+        return super.equals(other)
+            && Objects.equals(domainType, cdt.domainType)
+            && Objects.equals(parameters, cdt.parameters)
+            && Objects.equals(coverages, cdt.coverages)
+            && Objects.equals(parameterGroups, cdt.parameterGroups)
+            && Objects.equals(referencing, cdt.referencing);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                domainType,
+                coverages,
+                parameters,
+                parameterGroups,
+                referencing);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CoverageJsonObject.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CoverageJsonObject.java
new file mode 100644
index 0000000000..1b80490a85
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CoverageJsonObject.java
@@ -0,0 +1,55 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbSubtype;
+import jakarta.json.bind.annotation.JsonbTypeInfo;
+
+/**
+ * A CoverageJSON document can be extended with custom members and types in a
+ * robust and interoperable way. For that, it makes use of absolute URIs and
+ * compact URIs (prefix:suffix) in order to avoid conflicts with other extensions
+ * and future versions of the format. A central registry of compact URI prefixes
+ * is provided which anyone can extend and which is a simple mapping from compact
+ * URI prefix to namespace URI in order to avoid collisions with other extensions
+ * that are based on compact URIs as well. Extensions that do not follow this
+ * approach MAY use simple names instead of absolute or compact URIs but have to
+ * accept the consequence of the document being less interoperable and future-proof.
+ * In certain use cases this is not an issue and may be a preferred solution for
+ * simplicity reasons, for example, if such CoverageJSON documents are only used
+ * internally and are not meant to be shared to a wider audience.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbTypeInfo ( key = "type",value = {
+    @JsonbSubtype(alias = "Coverage", type = Coverage.class),
+    @JsonbSubtype(alias = "CoverageCollection", type = CoverageCollection.class),
+    @JsonbSubtype(alias = "Domain", type = Domain.class),
+    @JsonbSubtype(alias = "NdArray", type = NdArray.class),
+    @JsonbSubtype(alias = "Parameter", type = Parameter.class),
+    @JsonbSubtype(alias = "ParameterGroup", type = ParameterGroup.class),
+
+    //system subtypes
+    @JsonbSubtype(alias = "GeographicCRS", type = GeographicCRS.class),
+    @JsonbSubtype(alias = "ProjectedCRS", type = ProjectedCRS.class),
+    @JsonbSubtype(alias = "IdentifierRS", type = IdentifierRS.class),
+    @JsonbSubtype(alias = "VerticalCRS", type = VerticalCRS.class),
+    @JsonbSubtype(alias = "TemporalRS", type = TemporalRS.class)
+})
+public class CoverageJsonObject extends Dictionary<Object>{
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Dictionary.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Dictionary.java
new file mode 100644
index 0000000000..b46add8557
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Dictionary.java
@@ -0,0 +1,69 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.annotation.JsonbTransient;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @author Johann Sorel (Geomatys)
+ */
+public class Dictionary<T> {
+
+    /**
+     * could be anything.
+     * TODO find how to cath any other property as in Jackson @JsonAnyGetter and Johnzon @JohnzonAny
+     */
+    @JsonbTransient
+    public final LinkedHashMap<String, T> any = new LinkedHashMap<>();
+
+    public final Map<String, T> getAny() {
+        return this.any;
+    }
+
+    public final void setAnyProperty(String name, T value) {
+        this.any.put(name, value);
+    }
+
+    @Override
+    public String toString() {
+        try (Jsonb jsonb = JsonbBuilder.create()) {
+            return jsonb.toJson(this);
+        } catch (Exception ex) {
+            ex.printStackTrace();
+            return getClass().getName() + " : to_string_exception" + ex.getMessage();
+        }
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Dictionary)) return false;
+
+        final Dictionary cdt = ((Dictionary) other);
+        return Objects.equals(any, cdt.any);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(any);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Domain.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Domain.java
new file mode 100644
index 0000000000..5291a1b09f
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Domain.java
@@ -0,0 +1,90 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A domain object is a CoverageJSON object which defines a set of positions and
+ * their extent in one or more referencing systems.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","domainType","axes","referencing"})
+public final class Domain extends CoverageJsonObject {
+
+    public static final String DOMAINTYPE_GRID = "Grid";
+    public static final String DOMAINTYPE_VERTICALPROFILE = "VerticalProfile";
+    public static final String DOMAINTYPE_POINTSERIES = "PointSeries";
+    public static final String DOMAINTYPE_POINT = "Point";
+    public static final String DOMAINTYPE_MULTIPOINTSERIES = "MultiPointSeries";
+    public static final String DOMAINTYPE_MULTIPOINT = "MultiPoint";
+    public static final String DOMAINTYPE_POLYGONSERIES = "PolygonSeries";
+    public static final String DOMAINTYPE_POLYGON = "Polygon";
+    public static final String DOMAINTYPE_MULTIPOLYGONSERIES = "MultiPolygonSeries";
+    public static final String DOMAINTYPE_MULTIPOLYGON = "MultiPolygon";
+    public static final String DOMAINTYPE_TRAJECTORY = "Trajectory";
+    public static final String DOMAINTYPE_SECTION = "Section";
+
+    /**
+     * For interoperability reasons it is RECOMMENDED that a domain object has
+     * the member "domainType" with a string value to indicate that the domain
+     * follows a certain structure (e.g. a time series, a vertical profile, a
+     * spatio-temporal 4D grid). See the section Common Domain Types for details.
+     * Custom domain types may be used as recommended in the section Extensions.
+     */
+    public String domainType;
+    /**
+     * A domain object MUST have the member "axes" which has as value an object
+     * where each key is an axis identifier and each value an axis object as defined below.
+     */
+    public Axes axes;
+    /**
+     * A domain object MAY have the member "referencing" where the value is an
+     * array of reference system connection objects as defined below.
+     *
+     * A domain object MUST have a "referencing" member if the domain object is
+     * not part of a coverage collection or if the coverage collection does not
+     * have a "referencing" member.
+     */
+    public List<ReferenceSystemConnection> referencing;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Domain)) return false;
+
+        final Domain cdt = ((Domain) other);
+        return super.equals(other)
+            && Objects.equals(domainType, cdt.domainType)
+            && Objects.equals(axes, cdt.axes)
+            && Objects.equals(referencing, cdt.referencing);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                domainType,
+                axes,
+                referencing);
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/GeographicCRS.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/GeographicCRS.java
new file mode 100644
index 0000000000..0edc1a4420
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/GeographicCRS.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * Geographic CRSs anchor coordinate values to an ellipsoidal approximation of
+ * the Earth. They have coordinate axes of geodetic longitude and geodetic
+ * latitude, and perhaps height above the ellipsoid (i.e. they can be two- or three-dimensional).
+ * The origin of the CRS is on the surface of the ellipsoid.
+ *
+ * Note that sometimes (e.g. for numerical model data) the exact CRS may not be
+ * known or may be undefined. In this case the "id" may be omitted, but the "type"
+ * still indicates that this is a geographic CRS. Therefore clients can still use
+ * geodetic longitude, geodetic latitude (and maybe height) axes, even if they
+ * cannot accurately georeference the information.
+ * If a Coverage conforms to one of the defined domain types then the coordinate
+ * identifier "x" is used to denote geodetic longitude, "y" is used for geodetic
+ * latitude and "z" for ellipsoidal height.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","description"})
+public final class GeographicCRS extends CoverageJsonObject {
+
+    /**
+     * The object MAY have an "id" member, whose value MUST be a string and
+     * SHOULD be a common identifier for the reference system.
+     */
+    public String id;
+    /**
+     * The object MAY have a "description" member, where the value MUST be an
+     * i18n object, but no standardized content is interpreted from this description.
+     */
+    public I18N description;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof GeographicCRS)) return false;
+
+        final GeographicCRS cdt = ((GeographicCRS) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(description, cdt.description);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                description);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/I18N.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/I18N.java
new file mode 100644
index 0000000000..7ec02f650f
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/I18N.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
+import jakarta.json.bind.serializer.DeserializationContext;
+import jakarta.json.bind.serializer.JsonbDeserializer;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
+import java.lang.reflect.Type;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.sis.internal.coveragejson.binding.I18N.Serializer;
+import org.opengis.util.InternationalString;
+
+/**
+ * The special language tag "und" can be used to identify a value whose language
+ * is unknown or undetermined.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbTypeDeserializer(I18N.Deserializer.class)
+@JsonbTypeSerializer(Serializer.class)
+public final class I18N extends Dictionary<String> implements InternationalString {
+
+    public static final String UNDETERMINED = "und";
+
+    public I18N() {
+    }
+
+    public I18N(String lang, String text) {
+        setAnyProperty(lang, text);
+    }
+
+    private String getDefault() {
+        String str = any.get(UNDETERMINED);
+        if (str == null && !any.isEmpty()) str = any.get(any.keySet().iterator().next());
+        if (str == null) str = "";
+        return str;
+    }
+
+    public String toString() {
+        return getDefault();
+    }
+
+    @Override
+    public String toString(Locale locale) {
+        String str = any.get(locale.getLanguage());
+        if (str == null) str = any.get(locale.getISO3Language());
+        return getDefault();
+    }
+
+    @Override
+    public int length() {
+        return getDefault().length();
+    }
+
+    @Override
+    public char charAt(int index) {
+        return getDefault().charAt(index);
+    }
+
+    @Override
+    public CharSequence subSequence(int start, int end) {
+        return getDefault().subSequence(start, end);
+    }
+
+    @Override
+    public int compareTo(InternationalString o) {
+        return getDefault().compareTo(o.toString());
+    }
+
+    public static class Deserializer implements JsonbDeserializer<I18N> {
+        @Override
+        public I18N deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
+            final I18N candidate = new I18N();
+            while (parser.hasNext()) {
+                final JsonParser.Event event = parser.next();
+                if (event == JsonParser.Event.KEY_NAME) {
+                    // Deserialize inner object
+                    final String name = parser.getString();
+                    String value = ctx.deserialize(String.class, parser);
+                    candidate.setAnyProperty(name, value);
+                }
+            }
+            return candidate;
+        }
+    }
+
+    public static class Serializer implements JsonbSerializer<I18N> {
+
+        @Override
+        public void serialize(I18N ranges, JsonGenerator jg, SerializationContext sc) {
+            jg.writeStartObject();
+            for (Map.Entry<String,String> entry : ranges.any.entrySet()) {
+                jg.write(entry.getKey(), entry.getValue());
+            }
+            jg.writeEnd();
+        }
+
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/IdentifierRS.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/IdentifierRS.java
new file mode 100644
index 0000000000..8ff531369e
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/IdentifierRS.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * Identifier-based reference systems (identifier RS) .
+ *
+ * Coordinate values associated with an identifier RS MUST be strings.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","label","description","targetConcept","identifiers"})
+public final class IdentifierRS extends CoverageJsonObject {
+
+    /**
+     * An identifier RS object MAY have a member "id" where the value MUST be a
+     * string and SHOULD be a common identifier for the reference system.
+     */
+    public String id;
+    /**
+     * An identifier RS object MAY have a member "label" where the value MUST be
+     * an i18n object that is the name of the reference system.
+     */
+    public I18N label;
+    /**
+     * An identifier RS object MAY have a member "description" where the value
+     * MUST be an i18n object that is the (perhaps lengthy) description of the
+     * reference system.
+     */
+    public I18N description;
+    /**
+     * An identifier RS object MUST have a member "targetConcept"
+     */
+    public TargetConcept targetConcept;
+    /**
+     * An identifier RS object MAY have a member "identifiers".
+     */
+    public Identifiers identifiers;
+
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof IdentifierRS)) return false;
+
+        final IdentifierRS cdt = ((IdentifierRS) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(description, cdt.description)
+            && Objects.equals(targetConcept, cdt.targetConcept)
+            && Objects.equals(identifiers, cdt.identifiers);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                label,
+                description,
+                targetConcept,
+                identifiers);
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Identifiers.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Identifiers.java
new file mode 100644
index 0000000000..ac4f495183
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Identifiers.java
@@ -0,0 +1,29 @@
+/*
+ * 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.coveragejson.binding;
+
+
+/**
+ * An identifier RS object MAY have a member "identifiers" where the value is
+ * an object where each key is an identifier referenced by the identifier RS
+ * and each value an object describing the referenced concept, equal to "targetConcept".
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Identifiers extends Dictionary<TargetConcept> {
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/NdArray.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/NdArray.java
new file mode 100644
index 0000000000..dc1a6a2c1e
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/NdArray.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.internal.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A CoverageJSON object with the type "NdArray" is an NdArray object.
+ * It represents a multidimensional (>= 0D) array with named axes, encoded as a
+ * flat, one-dimensional JSON array in row-major order.
+ *
+ * Note that common JSON implementations use IEEE 754-2008 64-bit
+ * (double precision) floating point numbers as the data type for "values". Users
+ * SHOULD be aware of the limitations in precision when encoding numbers in this way.
+ * For example, when encoding integers, users SHOULD be aware that only values
+ * within the range [-253+1, 253-1] can be represented in a way that will ensure
+ * exact interoperability among such implementations [IETF RFC 7159].
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","dataType","axisNames","shape","values"})
+public final class NdArray extends CoverageJsonObject {
+
+    public static final String DATATYPE_FLOAT = "float";
+    public static final String DATATYPE_INTEGER = "integer";
+    public static final String DATATYPE_STRING = "string";
+
+    /**
+     * An NdArray object MUST have a member with the name "dataType" where the
+     * value is either "float", "integer", or "string" and MUST correspond to
+     * the data type of the non-null values in the "values" array.
+     */
+    public String dataType;
+    /**
+     * An NdArray object MAY have a member with the name "axisNames" where the
+     * value is an array of strings of the same length as "shape", such that
+     * each string assigns a name to the corresponding dimension. For 0D arrays,
+     * "axisNames" MAY be omitted (defaulting to []). For >= 1D arrays it MUST
+     * be included.
+     */
+    public String[] axisNames;
+    /**
+     * An NdArray object MAY have a member with the name "shape" where the value
+     * is an array of integers. For 0D arrays, "shape" MAY be omitted
+     * (defaulting to []). For >= 1D arrays it MUST be included.
+     *
+     * Where "shape" is present and non-empty, the product of its values MUST
+     * equal the number of elements in the "values" array.
+     */
+    public int[] shape;
+    /**
+     * An NdArray object MUST have a member with the name "values" where the
+     * value is a non-empty array of numbers and nulls, or strings and nulls,
+     * where nulls represent missing data.
+     *
+     * Zero-dimensional NdArrays MUST have exactly one item in the "values" array.
+     *
+     * Within the "values" array, the elements MUST be ordered such that the
+     * last dimension in "axisNames" varies fastest, i.e. row-major order.
+     * (This mimics the approach taken in NetCDF; see the example below.)
+     */
+    public List<Object> values; //because of null and string values
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof NdArray)) return false;
+
+        final NdArray cdt = ((NdArray) other);
+        return super.equals(other)
+            && Objects.equals(dataType, cdt.dataType)
+            && Arrays.equals(axisNames, cdt.axisNames)
+            && Arrays.equals(shape, cdt.shape)
+            && Objects.equals(values, cdt.values);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                dataType,
+                Arrays.hashCode(axisNames),
+                Arrays.hashCode(shape),
+                values);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ObservedProperty.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ObservedProperty.java
new file mode 100644
index 0000000000..62f49f163e
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ObservedProperty.java
@@ -0,0 +1,88 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Observed property is an object which MUST have the member "label" and which
+ * MAY have the members "id", "description", and "categories".
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"id","label","description","categories"})
+public final class ObservedProperty extends Dictionary<Object> {
+
+    /**
+     * If given, the value of "id" MUST be a string and SHOULD be a common
+     * identifier.
+     */
+    public String id;
+    /**
+     * The value of "label" MUST be an i18n object that is the name of the
+     * observed property and which SHOULD be short.
+     */
+    public I18N label;
+    /**
+     * If given, the value of "description" MUST be an i18n object with a
+     * textual description of the observed property.
+     */
+    public I18N description;
+    /**
+     *
+     * If given, the value of "categories" MUST be a non-empty array of category
+     * objects.
+     */
+    public List<Category> categories;
+
+    public ObservedProperty() {
+    }
+
+    public ObservedProperty(String id, I18N label, I18N description, List<Category> categories) {
+        this.id = id;
+        this.label = label;
+        this.description = description;
+        this.categories = categories;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof ObservedProperty)) return false;
+
+        final ObservedProperty cdt = ((ObservedProperty) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(description, cdt.description)
+            && Objects.equals(categories, cdt.categories);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                label,
+                description,
+                categories);
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Parameter.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Parameter.java
new file mode 100644
index 0000000000..dc5c6645e4
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Parameter.java
@@ -0,0 +1,91 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * A parameter object MAY have any number of members (name/value pairs).
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","label","description","unit","observedProperty","categoryEncoding"})
+public final class Parameter extends CoverageJsonObject {
+
+    /**
+     * A parameter object MAY have a member with the name "id" where the value
+     * MUST be a string and SHOULD be a common identifier.
+     */
+    public String id;
+    /**
+     * A parameter object MAY have a member with the name "label" where the value
+     * MUST be an i18n object that is the name of the parameter and which SHOULD be short.
+     * Note that this SHOULD be left out if it would be identical to the "label"
+     * of the "observedProperty" member.
+     */
+    public I18N label;
+    /**
+     * A parameter object MAY have a member with the name "description" where
+     * the value MUST be an i18n object which is a, perhaps lengthy, textual
+     * description of the parameter.
+     */
+    public I18N description;
+    /**
+     * A parameter object MAY have a member with the name "unit".
+     * A parameter object MUST NOT have a "unit" member if the "observedProperty"
+     * member has a "categories" member.
+     */
+    public Unit unit;
+    /**
+     * A parameter object MUST have a member with the name "observedProperty".
+     */
+    public ObservedProperty observedProperty;
+    /**
+     * A parameter object MAY have a member with the name "categoryEncoding".
+     */
+    public CategoryEncoding categoryEncoding;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Parameter)) return false;
+
+        final Parameter cdt = ((Parameter) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(description, cdt.description)
+            && Objects.equals(unit, cdt.unit)
+            && Objects.equals(observedProperty, cdt.observedProperty)
+            && Objects.equals(categoryEncoding, cdt.categoryEncoding);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                label,
+                description,
+                unit,
+                observedProperty,
+                categoryEncoding);
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ParameterGroup.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ParameterGroup.java
new file mode 100644
index 0000000000..d18ddfd3ed
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ParameterGroup.java
@@ -0,0 +1,86 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A parameter group object MUST have either or both the members "label" or/and "observedProperty".
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","label","description","observedProperty","members"})
+public final class ParameterGroup extends CoverageJsonObject {
+
+    /**
+     * A parameter group object MAY have a member with the name "id" where the
+     * value MUST be a string and SHOULD be a common identifier.
+     */
+    public String id;
+    /**
+     * A parameter group object MAY have a member with the name "label" where
+     * the value MUST be an i18n object that is the name of the parameter group
+     * and which SHOULD be short. Note that this SHOULD be left out if it would
+     * be identical to the "label" of the "observedProperty" member.
+     */
+    public I18N label;
+    /**
+     * A parameter group object MAY have a member with the name "description"
+     * where the value MUST be an i18n object which is a, perhaps lengthy,
+     * textual description of the parameter group.
+     */
+    public String description;
+    /**
+     * A parameter group object MAY have a member with the name "observedProperty"
+     * where the value is an object as specified for parameter objects.
+     */
+    public ObservedProperty observedProperty;
+    /**
+     * A parameter group object MUST have a member with the name "members"
+     * where the value is a non-empty array of parameter identifiers
+     * (see 6.3 Coverage objects).
+     */
+    public String[] members;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof ParameterGroup)) return false;
+
+        final ParameterGroup cdt = ((ParameterGroup) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(description, cdt.description)
+            && Objects.equals(observedProperty, cdt.observedProperty)
+            && Arrays.equals(members, cdt.members);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                label,
+                description,
+                observedProperty,
+                Arrays.hashCode(members));
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Parameters.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Parameters.java
new file mode 100644
index 0000000000..7c29c5fb26
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Parameters.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
+import jakarta.json.bind.serializer.DeserializationContext;
+import jakarta.json.bind.serializer.JsonbDeserializer;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
+import jakarta.json.stream.JsonParser.Event;
+import java.lang.reflect.Type;
+import java.util.Map;
+import org.apache.sis.internal.coveragejson.binding.Parameters.Deserializer;
+import org.apache.sis.internal.coveragejson.binding.Parameters.Serializer;
+
+/**
+ * Constains a map of parameter objects.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbTypeDeserializer(Deserializer.class)
+@JsonbTypeSerializer(Serializer.class)
+public final class Parameters extends Dictionary<Parameter> {
+
+    public static class Deserializer implements JsonbDeserializer<Parameters> {
+        @Override
+        public Parameters deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
+            final Parameters parameters = new Parameters();
+            while (parser.hasNext()) {
+                final Event event = parser.next();
+                if (event == JsonParser.Event.KEY_NAME) {
+                    // Deserialize inner object
+                    final String name = parser.getString();
+                    final Parameter value = ctx.deserialize(Parameter.class, parser);
+                    parameters.setAnyProperty(name, value);
+                }
+            }
+            return parameters;
+        }
+    }
+
+    public static class Serializer implements JsonbSerializer<Parameters> {
+
+        @Override
+        public void serialize(Parameters parameters, JsonGenerator jg, SerializationContext sc) {
+            jg.writeStartObject();
+            for (Map.Entry<String,Parameter> entry : parameters.any.entrySet()) {
+                sc.serialize(entry.getKey(), entry.getValue(), jg);
+            }
+            jg.writeEnd();
+        }
+
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ProjectedCRS.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ProjectedCRS.java
new file mode 100644
index 0000000000..ebff794f48
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ProjectedCRS.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * Projected CRSs use two coordinates to denote positions on a Cartesian plane,
+ * which is derived from projecting the ellipsoid according to some defined transformation.
+ *
+ * If a Coverage conforms to one of the defined domain types then the coordinate
+ * identifier "x" is used to denote easting and "y" is used for northing.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","description"})
+public final class ProjectedCRS extends CoverageJsonObject {
+
+    /**
+     * The object MAY have an "id" member, whose value MUST be a string and
+     * SHOULD be a common identifier for the reference system.
+     */
+    public String id;
+    /**
+     * The object MAY have a "description" member, where the value MUST be an
+     * i18n object, but no standardized content is interpreted from this description.
+     */
+    public I18N description;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof ProjectedCRS)) return false;
+
+        final ProjectedCRS cdt = ((ProjectedCRS) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(description, cdt.description);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                description);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Ranges.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Ranges.java
new file mode 100644
index 0000000000..3a322ad65a
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Ranges.java
@@ -0,0 +1,67 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
+import jakarta.json.bind.serializer.DeserializationContext;
+import jakarta.json.bind.serializer.JsonbDeserializer;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
+import java.lang.reflect.Type;
+import java.util.Map.Entry;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbTypeDeserializer(Ranges.Deserializer.class)
+@JsonbTypeSerializer(Ranges.Serializer.class)
+public final class Ranges extends Dictionary<NdArray> {
+
+    public static class Deserializer implements JsonbDeserializer<Ranges> {
+        @Override
+        public Ranges deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
+            final Ranges candidate = new Ranges();
+            while (parser.hasNext()) {
+                final JsonParser.Event event = parser.next();
+                if (event == JsonParser.Event.KEY_NAME) {
+                    // Deserialize inner object
+                    final String name = parser.getString();
+                    final NdArray value = ctx.deserialize(NdArray.class, parser);
+                    candidate.setAnyProperty(name, value);
+                }
+            }
+            return candidate;
+        }
+    }
+
+    public static class Serializer implements JsonbSerializer<Ranges> {
+
+        @Override
+        public void serialize(Ranges ranges, JsonGenerator jg, SerializationContext sc) {
+            jg.writeStartObject();
+            for (Entry<String,NdArray> entry : ranges.any.entrySet()) {
+                sc.serialize(entry.getKey(), entry.getValue(), jg);
+            }
+            jg.writeEnd();
+        }
+
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ReferenceSystemConnection.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ReferenceSystemConnection.java
new file mode 100644
index 0000000000..c072bf206d
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ReferenceSystemConnection.java
@@ -0,0 +1,67 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A reference system connection object creates a link between values within
+ * domain axes and a reference system to be able to interpret those values, e.g.
+ * as coordinates in a certain coordinate reference system.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"coordinates","system"})
+public final class ReferenceSystemConnection extends Dictionary<Object> {
+
+    /**
+     * A reference system connection object MUST have a member "coordinates"
+     * which has as value an array of coordinate identifiers that are referenced
+     * in this object. Depending on the type of referencing, the ordering of the
+     * identifiers MAY be relevant, e.g. for 2D/3D coordinate reference systems.
+     * In this case, the order of the identifiers MUST match the order of axes
+     * in the coordinate reference system.
+     */
+    public List<String> coordinates;
+    /**
+     * A reference system connection object MUST have a member "system" whose
+     * value MUST be a Reference System Object.
+     */
+    public CoverageJsonObject system;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof ReferenceSystemConnection)) return false;
+
+        final ReferenceSystemConnection cdt = ((ReferenceSystemConnection) other);
+        return super.equals(other)
+            && Objects.equals(system, cdt.system)
+            && Objects.equals(coordinates, cdt.coordinates);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                system,
+                coordinates);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Symbol.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Symbol.java
new file mode 100644
index 0000000000..5621f9be4d
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Symbol.java
@@ -0,0 +1,60 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"value","type"})
+public final class Symbol extends Dictionary<Object> {
+
+    /**
+     * "value" is the symbolic unit notation
+     */
+    public String value;
+    /**
+     * "type" references the unit serialization scheme that is used. "type" MUST
+     * HAVE the value "http://www.opengis.net/def/uom/UCUM/" if UCUM is used, or
+     * a custom value as recommended in section Extensions.
+     */
+    public String type;
+
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Symbol)) return false;
+
+        final Symbol cdt = ((Symbol) other);
+        return super.equals(other)
+            && Objects.equals(value, cdt.value)
+            && Objects.equals(type, cdt.type);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                value,
+                type);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TargetConcept.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TargetConcept.java
new file mode 100644
index 0000000000..e56b177317
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TargetConcept.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * TargetConcept is an object that MUST have a member "label" and MAY have a member
+ * "description" where the value of each MUST be an i18n object that is the
+ * name or description, respectively, of the concept which is referenced in the system.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"label","description"})
+public final class TargetConcept extends Dictionary<Object> {
+
+    public I18N label;
+    public I18N description;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof TargetConcept)) return false;
+
+        final TargetConcept cdt = ((TargetConcept) other);
+        return super.equals(other)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(description, cdt.description);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                label,
+                description);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TemporalRS.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TemporalRS.java
new file mode 100644
index 0000000000..d3eacc3159
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TemporalRS.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * Time is referenced by a temporal reference system (temporal RS). In the current
+ * version of this Community Standard, only a string-based notation for time
+ * values is defined. Future versions of this Community Standard may allow for
+ * alternative notations, such as recording time values as numeric offsets from
+ * a given temporal datum (e.g. “days since 1970-01-01”).
+ *
+ * If the calendar is based on years, months, days, then the referenced values
+ * SHOULD use one of the following ISO8601-based lexical representations:
+ * YYYY
+ * ±XYYYY (where X stands for extra year digits)
+ * YYYY-MM
+ * YYYY-MM-DD
+ * YYYY-MM-DDTHH:MM:SS[.F]Z where Z is either “Z” or a time scale offset +|-HH:MM
+ *
+ * If calendar dates with reduced precision are used in a lexical representation
+ * (e.g. "2016"), then a client SHOULD interpret those dates in that reduced precision.
+ *
+ * If "type" is "TemporalRS" and "calendar" is "Gregorian", then the above lexical
+ * representation MUST be used.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","calendar","timeScale"})
+public final class TemporalRS extends CoverageJsonObject {
+
+    /**
+     * A temporal RS object MUST have a member "calendar" with value "Gregorian" or a URI.
+     * If the Gregorian calendar is used, then "calendar" MUST have the value "Gregorian" and cannot be a URI.
+     */
+    public String calendar;
+    /**
+     * A temporal RS object MAY have a member "timeScale" with a URI as value.
+     * If omitted, the time scale defaults to "UTC". If the time scale is UTC,
+     * the "timeScale" member MUST be omitted.
+     */
+    public String timeScale;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof TemporalRS)) return false;
+
+        final TemporalRS cdt = ((TemporalRS) other);
+        return super.equals(other)
+            && Objects.equals(calendar, cdt.calendar)
+            && Objects.equals(timeScale, cdt.timeScale);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                calendar,
+                timeScale);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TileSet.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TileSet.java
new file mode 100644
index 0000000000..4e626c2266
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TileSet.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"tileShape","urlTemplate"})
+public final class TileSet extends Dictionary<Object> {
+
+    /**
+     * A TileSet object MUST have a member with the name "tileShape" where
+     * the value is an array of the same length as "shape" and where each
+     * array element is either null or an integer lower or equal than the
+     * corresponding element in "shape". A null value denotes that the axis
+     * is not tiled.
+     */
+    public Integer[] tileShape;
+    /**
+     * A TileSet object MUST have a member with the name "urlTemplate" where
+     * the value is a Level 1 URI template as defined in RFC 6570 .
+     * The URI template MUST contain a variable for each axis name whose
+     * corresponding element in "tileShape" is not null. A variable for an
+     * axis of total size totalSize (from "shape") and tile size tileSize
+     * (from "tileShape") has as value one of the integers 0, 1, …​, q + r - 1
+     * where q and r are the quotient and remainder obtained by dividing
+     * totalSize by tileSize. Each URI that can be generated from the URI
+     * template MUST resolve to an NdArray CoverageJSON document where the
+     * members "dataType" and "axisNames`" are identical to the ones of the
+     * TiledNdArray object, and where each value of `"shape" is an integer
+     * equal, or lower if an edge tile, to the corresponding element in
+     * "tileShape" while replacing null with the corresponding element of
+     * "shape" of the TiledNdArray.
+     */
+    public String urlTemplate;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof TileSet)) return false;
+
+        final TileSet cdt = ((TileSet) other);
+        return super.equals(other)
+            && Objects.equals(urlTemplate, cdt.urlTemplate)
+            && Arrays.equals(tileShape, cdt.tileShape);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                urlTemplate,
+                Arrays.hashCode(tileShape));
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TiledNdArray.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TiledNdArray.java
new file mode 100644
index 0000000000..43d7f710c7
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TiledNdArray.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A CoverageJSON object with the type "TiledNdArray" is a TiledNdArray object.
+ * It represents a multidimensional (>= 1D) array with named axes that is split
+ * up into sets of linked NdArray documents. Each tileset typically covers a
+ * specific data access scenario, for example, loading a single time slice of a
+ * grid vs. loading a time series of a spatial subset of a grid.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"dataType","shape","axisNames","tileSets"})
+public final class TiledNdArray extends Dictionary<Object> {
+
+    /**
+     * A TiledNdArray object MUST have a member with the name "dataType" where
+     * the value is either "float", "integer", or "string".
+     */
+    public String dataType;
+    /**
+     * A TiledNdArray object MUST have a member with the name "shape" where the
+     * value is a non-empty array of integers.
+     */
+    public int[] shape;
+    /**
+     * A TiledNdArray object MUST have a member with the name "axisNames" where
+     * the value is a string array of the same length as "shape".
+     */
+    public String[] axisNames;
+    /**
+     * A TiledNdArray object MUST have a member with the name "tileSets" where
+     * the value is a non-empty array of TileSet objects.
+     */
+    public TileSet[] tileSets;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof TiledNdArray)) return false;
+
+        final TiledNdArray cdt = ((TiledNdArray) other);
+        return super.equals(other)
+            && Objects.equals(dataType, cdt.dataType)
+            && Arrays.equals(shape, cdt.shape)
+            && Arrays.equals(axisNames, cdt.axisNames)
+            && Arrays.equals(tileSets, cdt.tileSets);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                dataType,
+                Arrays.hashCode(shape),
+                Arrays.hashCode(axisNames),
+                Arrays.hashCode(tileSets));
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Unit.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Unit.java
new file mode 100644
index 0000000000..78b94bacce
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Unit.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * A "unit" where the value is an object which MUST have either or both the members
+ * "label" or/and "symbol".
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"id","label","symbol"})
+public final class Unit extends Dictionary<Object> {
+
+    /**
+     * MAY have the member "id".
+     * If given, the value of "id" MUST be a string and
+     * SHOULD be a common identifier. It is RECOMMENDED to reference a unit
+     * serialization scheme to allow automatic unit conversion.
+     */
+    public String id;
+    /**
+     * If given, the value of "label" MUST be an i18n object of the name of
+     * the unit and SHOULD be short.
+     */
+    public I18N label;
+    /**
+     * If given, the value of "symbol" MUST either be a string of the symbolic notation of the unit,
+     * or an object with the members "value" and "type".
+     */
+    public Object symbol;
+
+    public Unit() {
+    }
+
+    public Unit(String id, I18N label, Object symbol) {
+        this.id = id;
+        this.label = label;
+        this.symbol = symbol;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Unit)) return false;
+
+        final Unit cdt = ((Unit) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(symbol, cdt.symbol);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                label,
+                symbol);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/VerticalCRS.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/VerticalCRS.java
new file mode 100644
index 0000000000..d3090a9670
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/VerticalCRS.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on anz "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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * Vertical CRSs use a single coordinate to denote some measure of height or depth,
+ * usually approximately oriented with gravity.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","description"})
+public final class VerticalCRS extends CoverageJsonObject {
+
+    /**
+     * The object MAY have an "id" member, whose value MUST be a string and
+     * SHOULD be a common identifier for the reference system.
+     */
+    public String id;
+    /**
+     * The object MAY have a "description" member, where the value MUST be an
+     * i18n object, but no standardised content is interpreted from this description.
+     */
+    public I18N description;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof VerticalCRS)) return false;
+
+        final VerticalCRS cdt = ((VerticalCRS) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(description, cdt.description);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                description);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider b/storage/sis-coveragejson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider
new file mode 100644
index 0000000000..458f567f12
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider
@@ -0,0 +1 @@
+org.apache.sis.internal.coveragejson.CoverageJsonStoreProvider
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreTest.java b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreTest.java
new file mode 100644
index 0000000000..23fe71e7fd
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.coveragejson;
+
+import jakarta.json.bind.JsonbBuilder;
+import java.awt.image.Raster;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.test.TestCase;
+import org.eclipse.yasson.internal.JsonBindingBuilder;
+import org.junit.Assert;
+import org.junit.Test;
+import org.opengis.metadata.spatial.DimensionNameType;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class CoverageJsonStoreTest extends TestCase {
+
+    /**
+     * Test coverage example from https://covjson.org/playground/.
+     */
+    @Test
+    public void testCoverageXYZT() throws Exception {
+
+        try (final DataStore store = new CoverageJsonStoreProvider().open(new StorageConnector(CoverageJsonStoreTest.class.getResource("coverage_xyzt.json")))) {
+
+            //test grid coverage resource exist
+            Assert.assertTrue(store instanceof Aggregate);
+            final Aggregate aggregate = (Aggregate) store;
+            Assert.assertEquals(1, aggregate.components().size());
+            final Resource candidate = aggregate.components().iterator().next();
+            Assert.assertTrue(candidate instanceof GridCoverageResource);
+            final GridCoverageResource gcr = (GridCoverageResource) candidate;
+
+            JsonbBuilder jcb = new JsonBindingBuilder();
+            { //test grid geometry
+                final GridGeometry result = gcr.getGridGeometry();
+                System.out.println(result);
+
+                Assert.assertEquals(4, result.getDimension());
+
+                final GridExtent expectedExtent = new GridExtent(new DimensionNameType[]{
+                    DimensionNameType.valueOf("x"),
+                    DimensionNameType.valueOf("y"),
+                    DimensionNameType.valueOf("z"),
+                    DimensionNameType.valueOf("t")},
+                        new long[]{0,0,0,0}, new long[]{2,1,0,0}, true);
+                Assert.assertEquals(expectedExtent, result.getExtent());
+                Assert.assertEquals(CRS.compound(CommonCRS.WGS84.geographic3D(), CommonCRS.Temporal.JAVA.crs()), result.getCoordinateReferenceSystem());
+                //TODO test transform
+            }
+
+
+            {   //test data
+                GridCoverage coverage = gcr.read(null);
+                Raster data = coverage.render(null).getData();
+            }
+        }
+
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonTestSuite.java b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonTestSuite.java
new file mode 100644
index 0000000000..3de67e2b7d
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonTestSuite.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.internal.coveragejson;
+
+import org.apache.sis.internal.coveragejson.binding.BindingTest;
+import org.apache.sis.test.TestSuite;
+import org.junit.BeforeClass;
+import org.junit.runners.Suite;
+
+
+/**
+ * All tests from the {@code sis-coveragejson} module, in rough dependency order.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@Suite.SuiteClasses({
+    BindingTest.class,
+    CoverageJsonStoreTest.class
+})
+public final class CoverageJsonTestSuite extends TestSuite {
+    /**
+     * Verifies the list of tests before to run the suite.
+     * See {@link #verifyTestList(Class, Class[])} for more information.
+     */
+    @BeforeClass
+    public static void verifyTestList() {
+        assertNoMissingTest(CoverageJsonTestSuite.class);
+        verifyTestList(CoverageJsonTestSuite.class);
+    }
+}
diff --git a/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/binding/BindingTest.java b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/binding/BindingTest.java
new file mode 100644
index 0000000000..6455364261
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/binding/BindingTest.java
@@ -0,0 +1,234 @@
+/*
+ * 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 anz "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.coveragejson.binding;
+
+import jakarta.json.JsonObject;
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.sis.test.TestCase;
+import org.eclipse.yasson.YassonConfig;
+import org.junit.AfterClass;
+import static org.junit.Assert.*;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Test coverage-json bindings.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class BindingTest extends TestCase {
+
+    private static final JsonbConfig CONFIG = new YassonConfig().withFormatting(true);
+
+    private static Jsonb jsonb;
+
+    public static String readResource(String path) throws IOException {
+        final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+        int nRead;
+        byte[] data = new byte[16384];
+        try (InputStream in = BindingTest.class.getResourceAsStream(path)) {
+            while ((nRead = in.read(data, 0, data.length)) != -1) {
+              buffer.write(data, 0, nRead);
+            }
+        }
+        buffer.flush();
+        return new String(buffer.toByteArray(), StandardCharsets.UTF_8);
+    }
+
+    private void compare(String jsonpath, Object expected) throws IOException {
+        String json = readResource(jsonpath);
+        //reformat it the same way.
+        JsonObject map = jsonb.fromJson(json, JsonObject.class);
+        String formattedJson = jsonb.toJson(map);
+
+        final Object candidate = jsonb.fromJson(json, expected.getClass());
+        expected.equals(candidate);
+        assertEquals(expected, candidate);
+        assertEquals(formattedJson, jsonb.toJson(candidate));
+    }
+
+    @BeforeClass
+    public static void beforeClass() {
+        jsonb = JsonbBuilder.create(CONFIG);
+    }
+
+    @AfterClass
+    public static void afterClass() throws Exception {
+        jsonb.close();
+    }
+
+    @Test
+    public void testAxeBounds() throws Exception {
+        final Axe expected = new Axe();
+        expected.values = asList(20,21);
+        expected.bounds = asList(19.5,20.5,20.5,21.5);
+        compare("axe_bounds.json", expected);
+    }
+
+    @Test
+    public void testAxePolygon() throws Exception {
+        final Axe expected = new Axe();
+        expected.dataType = "polygon";
+        expected.coordinates = Arrays.asList("x","y");
+        expected.values = Arrays.asList(
+            Arrays.asList(
+                Arrays.asList(
+                    asList(100.0, 0.0),
+                    asList(101.0, 0.0),
+                    asList(101.0, 1.0),
+                    asList(100.0, 1.0),
+                    asList(100.0, 0.0)
+                )
+            )
+        );
+        compare("axe_polygon.json", expected);
+    }
+
+    @Test
+    public void testAxeRegular() throws Exception {
+        final Axe expected = new Axe();
+        expected.start = 0.0;
+        expected.stop = 5.0;
+        expected.num = 6;
+        compare("axe_regular.json", expected);
+    }
+
+    @Test
+    public void testAxeTuples() throws Exception {
+        final Axe expected = new Axe();
+        expected.dataType = "tuple";
+        expected.coordinates = Arrays.asList("t","x","y");
+        expected.values = Arrays.asList(
+                asList("2008-01-01T04:00:00Z",1,20),
+                asList("2008-01-01T04:30:00Z",2,21)
+        );
+        compare("axe_tuples.json", expected);
+    }
+
+    @Test
+    public void testCoverageVerticalProfile() throws Exception {
+
+        final GeographicCRS geoCrs = new GeographicCRS();
+        geoCrs.id = "http://www.opengis.net/def/crs/OGC/1.3/CRS84";
+
+        final VerticalCRS zCrs = new VerticalCRS();
+        //"cs":{"csAxes":[{"name":{"en":"Pressure"},"direction":"down","unit":{"symbol":"Pa"}}]}
+        final Map<String,Object> axe = new LinkedHashMap<>();
+        axe.put("name", Map.of("en", "Pressure"));
+        axe.put("direction", "down");
+        axe.put("unit", Map.of("symbol", "Pa"));
+        final Map<String,Object> cs = new HashMap<>();
+        cs.put("csAxes", Arrays.asList(axe));
+        //zCrs.setAnyProperty("cs", cs); //TODO undefined attributes ignored
+
+        final TemporalRS tCrs = new TemporalRS();
+        tCrs.calendar = "Gregorian";
+
+        final ReferenceSystemConnection georsc = new ReferenceSystemConnection();
+        georsc.coordinates = Arrays.asList("x", "y");
+        georsc.system = geoCrs;
+
+        final ReferenceSystemConnection zrsc = new ReferenceSystemConnection();
+        zrsc.coordinates = Arrays.asList("z");
+        zrsc.system = zCrs;
+
+        final ReferenceSystemConnection trsc = new ReferenceSystemConnection();
+        trsc.coordinates = Arrays.asList("t");
+        trsc.system = tCrs;
+
+        final Domain domain = new Domain();
+        domain.domainType = "VerticalProfile";
+        domain.axes = new Axes();
+        domain.axes.x = new Axe();
+        domain.axes.y = new Axe();
+        domain.axes.z = new Axe();
+        domain.axes.t = new Axe();
+        domain.axes.x.values = asList(-10.1);
+        domain.axes.y.values = asList(-40.2);
+        domain.axes.z.values = asList(5.4562, 8.9282);
+        domain.axes.t.values = asList("2013-01-13T11:12:20Z");
+        domain.referencing = Arrays.asList(georsc, zrsc, trsc);
+
+        final Parameter PSAL = new Parameter();
+        PSAL.description = new I18N("en", "The measured salinity, in practical salinity units (psu) of the sea water ");
+        PSAL.unit = new Unit(null,null,"psu");
+        PSAL.observedProperty = new ObservedProperty("http://vocab.nerc.ac.uk/standard_name/sea_water_salinity/", new I18N("en", "Sea Water Salinity"), null, null);
+
+        final Parameter POTM = new Parameter();
+        POTM.description = new I18N("en", "The potential temperature, in degrees celcius, of the sea water");
+        POTM.unit = new Unit(null,null,"°C");
+        POTM.observedProperty = new ObservedProperty("http://vocab.nerc.ac.uk/standard_name/sea_water_potential_temperature/", new I18N("en", "Sea Water Potential Temperature"), null, null);
+
+        final Parameters parameters = new Parameters();
+        parameters.setAnyProperty("PSAL", PSAL);
+        parameters.setAnyProperty("POTM", POTM);
+
+        final NdArray PSALr = new NdArray();
+        PSALr.dataType ="float";
+        PSALr.shape = new int[]{2};
+        PSALr.axisNames = new String[]{"z"};
+        PSALr.values = asList(43.9599, 43.9599);
+
+        final NdArray POTMr = new NdArray();
+        POTMr.dataType ="float";
+        POTMr.shape = new int[]{2};
+        POTMr.axisNames = new String[]{"z"};
+        POTMr.values = asList(23.8, 23.7);
+
+        final Ranges ranges = new Ranges();
+        ranges.setAnyProperty("PSAL", PSALr);
+        ranges.setAnyProperty("POTM", POTMr);
+
+        final Coverage expected = new Coverage();
+        expected.domain = domain;
+        expected.parameters = parameters;
+        expected.ranges = ranges;
+
+        compare("coverage_vertical_profile_nocs.json", expected);
+    }
+
+    /**
+     * Convert numeric values to BigDecimal for equality tests.
+     */
+    private static List<Object> asList(Object... array) {
+        final List<Object> lst = new ArrayList<>(array.length);
+        for (int i=0;i<array.length;i++) {
+            if (array[i] instanceof Integer) {
+                lst.add(BigDecimal.valueOf((Integer) array[i]));
+            } else if (array[i] instanceof Double) {
+                lst.add(BigDecimal.valueOf((Double) array[i]));
+            } else {
+                lst.add(array[i]);
+            }
+        }
+        return lst;
+    }
+}
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_bounds.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_bounds.json
new file mode 100644
index 0000000000..a4d3c7c93b
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_bounds.json
@@ -0,0 +1,12 @@
+{
+  "values":[
+    20,
+    21
+  ],
+  "bounds":[
+    19.5,
+    20.5,
+    20.5,
+    21.5
+  ]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_polygon.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_polygon.json
new file mode 100644
index 0000000000..8fbaf14501
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_polygon.json
@@ -0,0 +1,33 @@
+{
+  "dataType":"polygon",
+  "coordinates":[
+    "x",
+    "y"
+  ],
+  "values":[
+    [
+      [
+        [
+          100.0,
+          0.0
+        ],
+        [
+          101.0,
+          0.0
+        ],
+        [
+          101.0,
+          1.0
+        ],
+        [
+          100.0,
+          1.0
+        ],
+        [
+          100.0,
+          0.0
+        ]
+      ]
+    ]
+  ]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_regular.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_regular.json
new file mode 100644
index 0000000000..010b46d9a9
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_regular.json
@@ -0,0 +1,5 @@
+{
+  "start": 0.0,
+  "stop": 5.0,
+  "num": 6
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_tuples.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_tuples.json
new file mode 100644
index 0000000000..79918750f3
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_tuples.json
@@ -0,0 +1,20 @@
+{
+  "dataType":"tuple",
+  "coordinates":[
+    "t",
+    "x",
+    "y"
+  ],
+  "values":[
+    [
+      "2008-01-01T04:00:00Z",
+      1,
+      20
+    ],
+    [
+      "2008-01-01T04:30:00Z",
+      2,
+      21
+    ]
+  ]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coverage_vertical_profile.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coverage_vertical_profile.json
new file mode 100644
index 0000000000..a975d21e4c
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coverage_vertical_profile.json
@@ -0,0 +1,91 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type" : "Domain",
+    "domainType" : "VerticalProfile",
+    "axes": {
+      "x" : { "values": [-10.1] },
+      "y" : { "values": [ -40.2] },
+      "z" : { "values": [
+              5.4562, 8.9282 ] },
+      "t" : { "values": ["2013-01-13T11:12:20Z"] }
+    },
+    "referencing": [{
+      "coordinates": ["x","y"],
+      "system": {
+        "type": "GeographicCRS",
+        "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
+      }
+    }, {
+      "coordinates": ["z"],
+      "system": {
+        "type": "VerticalCRS",
+        "cs": {
+          "csAxes": [{
+            "name": {
+              "en": "Pressure"
+            },
+            "direction": "down",
+            "unit": {
+              "symbol": "Pa"
+            }
+          }]
+        }
+      }
+    }, {
+      "coordinates": ["t"],
+      "system": {
+        "type": "TemporalRS",
+        "calendar": "Gregorian"
+      }
+    }]
+  },
+  "parameters" : {
+    "PSAL": {
+      "type" : "Parameter",
+      "description" : {
+        "en": "The measured salinity, in practical salinity units (psu) of the sea water "
+      },
+      "unit" : {
+        "symbol" : "psu"
+      },
+      "observedProperty" : {
+        "id" : "http://vocab.nerc.ac.uk/standard_name/sea_water_salinity/",
+        "label" : {
+          "en": "Sea Water Salinity"
+        }
+      }
+    },
+    "POTM": {
+      "type" : "Parameter",
+      "description" : {
+        "en": "The potential temperature, in degrees celcius, of the sea water"
+      },
+      "unit" : {
+        "symbol" : "°C"
+      },
+      "observedProperty" : {
+        "id" : "http://vocab.nerc.ac.uk/standard_name/sea_water_potential_temperature/",
+        "label" : {
+          "en": "Sea Water Potential Temperature"
+        }
+      }
+    }
+  },
+  "ranges" : {
+    "PSAL" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["z"],
+      "shape": [2],
+      "values" : [ 43.9599, 43.9599 ]
+    },
+    "POTM" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["z"],
+      "shape": [2],
+      "values" : [ 23.8, 23.7 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coverage_vertical_profile_nocs.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coverage_vertical_profile_nocs.json
new file mode 100644
index 0000000000..fd77ff48d6
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coverage_vertical_profile_nocs.json
@@ -0,0 +1,80 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type" : "Domain",
+    "domainType" : "VerticalProfile",
+    "axes": {
+      "x" : { "values": [-10.1] },
+      "y" : { "values": [ -40.2] },
+      "z" : { "values": [
+              5.4562, 8.9282 ] },
+      "t" : { "values": ["2013-01-13T11:12:20Z"] }
+    },
+    "referencing": [{
+      "coordinates": ["x","y"],
+      "system": {
+        "type": "GeographicCRS",
+        "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
+      }
+    }, {
+      "coordinates": ["z"],
+      "system": {
+        "type": "VerticalCRS"
+      }
+    }, {
+      "coordinates": ["t"],
+      "system": {
+        "type": "TemporalRS",
+        "calendar": "Gregorian"
+      }
+    }]
+  },
+  "parameters" : {
+    "PSAL": {
+      "type" : "Parameter",
+      "description" : {
+        "en": "The measured salinity, in practical salinity units (psu) of the sea water "
+      },
+      "unit" : {
+        "symbol" : "psu"
+      },
+      "observedProperty" : {
+        "id" : "http://vocab.nerc.ac.uk/standard_name/sea_water_salinity/",
+        "label" : {
+          "en": "Sea Water Salinity"
+        }
+      }
+    },
+    "POTM": {
+      "type" : "Parameter",
+      "description" : {
+        "en": "The potential temperature, in degrees celcius, of the sea water"
+      },
+      "unit" : {
+        "symbol" : "°C"
+      },
+      "observedProperty" : {
+        "id" : "http://vocab.nerc.ac.uk/standard_name/sea_water_potential_temperature/",
+        "label" : {
+          "en": "Sea Water Potential Temperature"
+        }
+      }
+    }
+  },
+  "ranges" : {
+    "PSAL" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["z"],
+      "shape": [2],
+      "values" : [ 43.9599, 43.9599 ]
+    },
+    "POTM" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["z"],
+      "shape": [2],
+      "values" : [ 23.8, 23.7 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coveragecollection.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coveragecollection.json
new file mode 100644
index 0000000000..e1af6e39d2
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coveragecollection.json
@@ -0,0 +1,92 @@
+{
+  "type" : "CoverageCollection",
+  "domainType" : "VerticalProfile",
+  "parameters" : {
+    "PSAL": {
+      "type" : "Parameter",
+      "description" : {
+        "en": "The measured salinity, in practical salinity units (psu) of the sea water"
+      },
+      "unit" : {
+        "symbol" : "psu"
+      },
+      "observedProperty" : {
+        "id": "http://vocab.nerc.ac.uk/standard_name/sea_water_salinity/",
+        "label" : {
+          "en": "Sea Water Salinity"
+        }
+      }
+    }
+  },
+  "referencing": [{
+    "coordinates": ["x","y"],
+    "system": {
+      "type": "GeographicCRS",
+      "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
+    }
+  }, {
+    "coordinates": ["z"],
+    "system": {
+      "type": "VerticalCRS",
+      "cs": {
+        "csAxes": [{
+          "name": {
+            "en": "Pressure"
+          },
+          "direction": "down",
+          "unit": {
+            "symbol": "Pa"
+          }
+        }]
+      }
+    }
+  }, {
+    "coordinates": ["t"],
+    "system": {
+      "type": "TemporalRS",
+      "calendar": "Gregorian"
+    }
+  }],
+  "coverages": [
+    {
+      "type" : "Coverage",
+      "domain" : {
+        "type": "Domain",
+        "axes": {
+          "x": { "values": [-10.1] },
+          "y": { "values": [-40.2] },
+          "z": { "values": [ 5, 8, 14 ] },
+          "t": { "values": ["2013-01-13T11:12:20Z"] }
+        }
+      },
+      "ranges" : {
+        "PSAL" : {
+          "type" : "NdArray",
+          "dataType": "float",
+          "shape": [3],
+          "axisNames": ["z"],
+          "values" : [ 43.7, 43.8, 43.9 ]
+        }
+      }
+    }, {
+      "type" : "Coverage",
+      "domain" : {
+        "type": "Domain",
+        "axes": {
+          "x": { "values": [-11.1] },
+          "y": { "values": [-45.2] },
+          "z": { "values": [ 4, 7, 9 ] },
+          "t": { "values": ["2013-01-13T12:12:20Z"] }
+        }
+      },
+      "ranges" : {
+        "PSAL" : {
+          "type" : "NdArray",
+          "dataType": "float",
+          "shape": [3],
+          "axisNames": ["z"],
+          "values" : [ 42.7, 41.8, 40.9 ]
+        }
+      }
+    }]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domain_grid.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domain_grid.json
new file mode 100644
index 0000000000..042aed9e21
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domain_grid.json
@@ -0,0 +1,26 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type" : "Domain",
+    "domainType" : "Grid",
+    "axes": {
+      "x": { "values": [1,2,3] },
+      "y": { "values": [20,21] },
+      "z": { "values": [1] },
+      "t": { "values": ["2008-01-01T04:00:00Z"] }
+    },
+    "referencing": []
+  },
+  "parameters" : {
+    "temperature": {}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["t", "z", "y", "x"],
+      "shape": [1, 1, 2, 3],
+      "values" : []
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domain_trajectory.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domain_trajectory.json
new file mode 100644
index 0000000000..43b421ded3
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domain_trajectory.json
@@ -0,0 +1,27 @@
+{
+  "type": "Domain",
+  "domainType": "Trajectory",
+  "axes": {
+    "composite": {
+      "dataType": "tuple",
+      "coordinates": ["t","x","y"],
+      "values": [
+        ["2008-01-01T04:00:00Z", 1, 20],
+        ["2008-01-01T04:30:00Z", 2, 21]
+      ]
+    }
+  },
+  "referencing": [{
+    "coordinates": ["t"],
+    "system": {
+      "type": "TemporalRS",
+      "calendar": "Gregorian"
+    }
+  }, {
+    "coordinates": ["x","y"],
+    "system": {
+      "type": "GeographicCRS",
+      "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
+    }
+  }]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_grid.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_grid.json
new file mode 100644
index 0000000000..ee521192e1
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_grid.json
@@ -0,0 +1,5 @@
+{
+  "start": 0,
+  "stop": 5,
+  "num": 6
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipoint.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipoint.json
new file mode 100644
index 0000000000..47ca3264b5
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipoint.json
@@ -0,0 +1,30 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "MultiPoint",
+    "axes": {
+      "t": { "values": ["2008-01-01T04:00:00Z"] },
+      "composite": {
+        "dataType": "tuple",
+        "coordinates": ["x","y","z"],
+        "values": [
+          [1, 20, 1],
+          [2, 21, 3]
+        ]
+      }
+    }
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["composite"],
+      "shape": [2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipointseries.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipointseries.json
new file mode 100644
index 0000000000..298b8cfb1c
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipointseries.json
@@ -0,0 +1,31 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "MultiPointSeries",
+    "axes": {
+      "t": { "values": ["2008-01-01T04:00:00Z", "2008-01-01T05:00:00Z"] },
+      "composite": {
+        "dataType": "tuple",
+        "coordinates": ["x","y","z"],
+        "values": [
+          [1, 20, 1],
+          [2, 21, 3],
+          [2, 20, 4]
+        ]
+      }
+    }
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["t", "composite"],
+      "shape": [2, 3],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipolygon.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipolygon.json
new file mode 100644
index 0000000000..e24f503100
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipolygon.json
@@ -0,0 +1,32 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "MultiPolygon",
+    "axes": {
+      "composite": {
+        "dataType": "polygon",
+        "coordinates": ["x","y"],
+        "values": [
+          [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ]  ],
+          [ [ [200.0, 10.0], [201.0, 10.0], [201.0, 11.0], [200.0, 11.0], [200.0, 10.0] ] ]
+        ]
+      },
+      "z": { "values": [2] },
+      "t": { "values": ["2008-01-01T04:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["composite"],
+      "shape": [2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipolygonseries.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipolygonseries.json
new file mode 100644
index 0000000000..a3ff18886f
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipolygonseries.json
@@ -0,0 +1,32 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "MultiPolygonSeries",
+    "axes": {
+      "composite": {
+        "dataType": "polygon",
+        "coordinates": ["x","y"],
+        "values": [
+          [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ]  ],
+          [ [ [200.0, 10.0], [201.0, 10.0], [201.0, 11.0], [200.0, 11.0], [200.0, 10.0] ] ]
+        ]
+      },
+      "z": { "values": [2] },
+      "t": { "values": ["2008-01-01T04:00:00Z", "2010-01-01T00:00:00Z", "2012-01-01T00:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["t", "composite"],
+      "shape": [3, 2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_point.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_point.json
new file mode 100644
index 0000000000..ab71e6e3f4
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_point.json
@@ -0,0 +1,24 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "Point",
+    "axes": {
+      "x": { "values": [1] },
+      "y": { "values": [20] },
+      "z": { "values": [1] },
+      "t": { "values": ["2008-01-01T04:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_pointseries.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_pointseries.json
new file mode 100644
index 0000000000..cdb412c1c0
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_pointseries.json
@@ -0,0 +1,26 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "PointSeries",
+    "axes": {
+      "x": { "values": [1] },
+      "y": { "values": [20] },
+      "z": { "values": [1] },
+      "t": { "values": ["2008-01-01T04:00:00Z","2008-01-01T05:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["t"],
+      "shape": [2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_polygon.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_polygon.json
new file mode 100644
index 0000000000..7e1053008f
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_polygon.json
@@ -0,0 +1,29 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "Polygon",
+    "axes": {
+      "composite": {
+        "dataType": "polygon",
+        "coordinates": ["x","y"],
+        "values": [
+          [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ]  ]
+        ]
+      },
+      "z": { "values": [2] },
+      "t": { "values": ["2008-01-01T04:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_polygonseries.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_polygonseries.json
new file mode 100644
index 0000000000..ca48a18c21
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_polygonseries.json
@@ -0,0 +1,31 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "PolygonSeries",
+    "axes": {
+      "composite": {
+        "dataType": "polygon",
+        "coordinates": ["x","y"],
+        "values": [
+          [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ]  ]
+        ]
+      },
+      "z": { "values": [2] },
+      "t": { "values": ["2008-01-01T04:00:00Z","2008-01-01T05:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["t"],
+      "shape": [2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_section.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_section.json
new file mode 100644
index 0000000000..48d360f6fa
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_section.json
@@ -0,0 +1,31 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "Section",
+    "axes": {
+      "z": { "values": [10,20,30] },
+      "composite": {
+        "dataType": "tuple",
+        "coordinates": ["t","x","y"],
+        "values": [
+          ["2008-01-01T04:00:00Z", 1, 20],
+          ["2008-01-01T04:30:00Z", 2, 21]
+        ]
+      }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["z", "composite"],
+      "shape": [3, 2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_trajectory.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_trajectory.json
new file mode 100644
index 0000000000..6c6937cfa0
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_trajectory.json
@@ -0,0 +1,30 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "Trajectory",
+    "axes": {
+      "composite": {
+        "dataType": "tuple",
+        "coordinates": ["t","x","y","z"],
+        "values": [
+          ["2008-01-01T04:00:00Z", 1, 20, 1],
+          ["2008-01-01T04:30:00Z", 2, 21, 3]
+        ]
+      }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["composite"],
+      "shape": [2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_vertical_profile.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_vertical_profile.json
new file mode 100644
index 0000000000..92ff3a7adf
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_vertical_profile.json
@@ -0,0 +1,26 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "VerticalProfile",
+    "axes": {
+      "x": { "values": [1] },
+      "y": { "values": [21] },
+      "z": { "values": [1,5,20] },
+      "t": { "values": ["2008-01-01T04:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["z"],
+      "shape": [3],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/geographiccrs_longlat.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/geographiccrs_longlat.json
new file mode 100644
index 0000000000..90814122a1
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/geographiccrs_longlat.json
@@ -0,0 +1,4 @@
+{
+  "type": "GeographicCRS",
+  "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/geographiccrs_longlatheight.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/geographiccrs_longlatheight.json
new file mode 100644
index 0000000000..0c736fd05e
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/geographiccrs_longlatheight.json
@@ -0,0 +1,4 @@
+{
+  "type": "GeographicCRS",
+  "id": "http://www.opengis.net/def/crs/EPSG/0/4979"
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/ndarray.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/ndarray.json
new file mode 100644
index 0000000000..da6f336406
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/ndarray.json
@@ -0,0 +1,10 @@
+{
+  "type": "NdArray",
+  "dataType": "float",
+  "shape": [4, 2],
+  "axisNames": ["y", "x"],
+  "values": [
+    12.3, 12.5, 11.5, 23.1,
+    null, null, 10.1, 9.1
+  ]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parameter_categoricaldata.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parameter_categoricaldata.json
new file mode 100644
index 0000000000..3afb80e7dc
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parameter_categoricaldata.json
@@ -0,0 +1,33 @@
+{
+  "type" : "Parameter",
+  "description" : {
+    "en": "The land cover category."
+  },
+  "observedProperty" : {
+    "id" : "http://example.com/land_cover",
+    "label" : {
+      "en": "Land Cover"
+    },
+    "description" : {
+      "en": "longer description..."
+    },
+    "categories": [{
+      "id": "http://example.com/land_cover/categories/grass",
+      "label": {
+        "en": "Grass"
+      },
+      "description": {
+        "en": "Very green grass."
+      }
+    }, {
+      "id": "http://example.com/land_cover/categories/forest",
+      "label": {
+        "en": "Forest"
+      }
+    }]
+  },
+  "categoryEncoding": {
+    "http://example.com/land_cover/categories/grass": 1,
+    "http://example.com/land_cover/categories/forest": [2,3]
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parameter_continuousdata.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parameter_continuousdata.json
new file mode 100644
index 0000000000..2ff6c92184
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parameter_continuousdata.json
@@ -0,0 +1,24 @@
+{
+  "type" : "Parameter",
+  "description" : {
+    "en": "The sea surface temperature in degrees Celsius."
+  },
+  "observedProperty" : {
+    "id" : "http://vocab.nerc.ac.uk/standard_name/sea_surface_temperature/",
+    "label" : {
+      "en": "Sea Surface Temperature"
+    },
+    "description" : {
+      "en": "The temperature of sea water near the surface (including the part under sea-ice, if any), and not the skin temperature."
+    }
+  },
+  "unit" : {
+    "label" : {
+      "en": "Degree Celsius"
+    },
+    "symbol": {
+      "value": "Cel",
+      "type": "http://www.opengis.net/def/uom/UCUM/"
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parametergroup_uncertainty.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parametergroup_uncertainty.json
new file mode 100644
index 0000000000..7b871885e3
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parametergroup_uncertainty.json
@@ -0,0 +1,13 @@
+{
+  "type": "ParameterGroup",
+  "label": {
+    "en": "Daily sea surface temperature with uncertainty information"
+  },
+  "observedProperty": {
+    "id": "http://vocab.nerc.ac.uk/standard_name/sea_surface_temperature/",
+    "label": {
+      "en": "Sea surface temperature"
+    }
+  },
+  "members": ["SST_mean", "SST_stddev"]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parametergroup_vectorquantity.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parametergroup_vectorquantity.json
new file mode 100644
index 0000000000..696eaaa3d9
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parametergroup_vectorquantity.json
@@ -0,0 +1,9 @@
+{
+  "type": "ParameterGroup",
+  "observedProperty": {
+    "label": {
+      "en": "Wind velocity"
+    }
+  },
+  "members": ["WIND_SPEED", "WIND_DIR"]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/projectedcrs_britishnationalgrid.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/projectedcrs_britishnationalgrid.json
new file mode 100644
index 0000000000..199edd7e4e
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/projectedcrs_britishnationalgrid.json
@@ -0,0 +1,4 @@
+{
+  "type": "ProjectedCRS",
+  "id": "http://www.opengis.net/def/crs/EPSG/0/27700"
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/reference_system_connection.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/reference_system_connection.json
new file mode 100644
index 0000000000..a94c3f36d5
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/reference_system_connection.json
@@ -0,0 +1,7 @@
+{
+  "coordinates": ["y","x","z"],
+  "system": {
+    "type": "GeographicCRS",
+    "id": "http://www.opengis.net/def/crs/EPSG/0/4979"
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/temporalrs.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/temporalrs.json
new file mode 100644
index 0000000000..ed5fb370a5
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/temporalrs.json
@@ -0,0 +1,4 @@
+{
+  "type": "TemporalRS",
+  "calendar": "Gregorian"
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/tiledndarray.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/tiledndarray.json
new file mode 100644
index 0000000000..34dd2674da
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/tiledndarray.json
@@ -0,0 +1,16 @@
+{
+  "type" : "TiledNdArray",
+  "dataType": "integer",
+  "axisNames": ["t", "y", "x"],
+  "shape": [2, 5, 10],
+  "tileSets": [{
+    "tileShape": [null, null, null],
+    "urlTemplate": "http://example.com/a/all.covjson"
+  }, {
+    "tileShape": [1, null, null],
+    "urlTemplate": "http://example.com/b/{t}.covjson"
+  }, {
+    "tileShape": [null, 2, 3],
+    "urlTemplate": "http://example.com/c/{y}-{x}.covjson"
+  }]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/verticalcrs_navd88.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/verticalcrs_navd88.json
new file mode 100644
index 0000000000..4bd091956f
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/verticalcrs_navd88.json
@@ -0,0 +1,4 @@
+{
+  "type": "VerticalCRS",
+  "id": "http://www.opengis.net/def/crs/EPSG/0/5703"
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/coverage_xyzt.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/coverage_xyzt.json
new file mode 100644
index 0000000000..332921893e
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/coverage_xyzt.json
@@ -0,0 +1,58 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type" : "Domain",
+    "domainType" : "Grid",
+    "axes": {
+      "x" : { "values": [-10,-5,0] },
+      "y" : { "values": [40,50] },
+      "z" : { "values": [ 5] },
+      "t" : { "values": ["2010-01-01T00:12:20Z"] }
+    },
+    "referencing": [{
+      "coordinates": ["y","x","z"],
+      "system": {
+        "type": "GeographicCRS",
+        "id": "http://www.opengis.net/def/crs/EPSG/0/4979"
+      }
+    }, {
+      "coordinates": ["t"],
+      "system": {
+        "type": "TemporalRS",
+        "calendar": "Gregorian"
+      }
+    }]
+  },
+  "parameters" : {
+    "ICEC": {
+      "type" : "Parameter",
+      "description": {
+      	"en": "Sea Ice concentration (ice=1;no ice=0)"
+      },
+      "unit" : {
+        "label": {
+          "en": "Ratio"
+        },
+        "symbol": {
+          "value": "1",
+          "type": "http://www.opengis.net/def/uom/UCUM/"
+        }
+      },
+      "observedProperty" : {
+        "id" : "http://vocab.nerc.ac.uk/standard_name/sea_ice_area_fraction/",
+        "label" : {
+          "en": "Sea Ice Concentration"
+        }
+      }
+    }
+  },
+  "ranges" : {
+    "ICEC" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["t","z","y","x"],
+      "shape": [1, 1, 2, 3],
+      "values" : [ 0.5, 0.6, 0.4, 0.6, 0.2, null ]
+    }
+  }
+}
\ No newline at end of file