opengeospatial / opengeospatial/CRS-JSON-Encoding

Experimenting with inheritance and unexpected members using JSON Schema ==> use ``"unevaluatedProperties": false``

Aperta
#61 0 commenti 1 reazione 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Lingua principale
Makefile
Stelle
0
Fork
2
Merge medio
1h 36m
PR unite (30g)
3

Descrizione

TLDR; summary: use "unevaluatedProperties": false when inheritance is involved (if using JSON Schema Draft >= 2019-09) to be able to reject unexpected members

Long version:

Let's play a bit with the example of "Best Practice for OGC - UML to JSON encoding rules" (https://portal.ogc.org/files/?artifact_id=108010&version=1#toc27), paragraph 7.3.3.4 "Inheritance"

Let's consider:

  • an instance document test.json containing:
{
  "propertyA": 2,
  "propertyB": "x"
}
  • the (actually "one among many possibilities") corresponding JSON schema, test.schema.json defining a class TypeA with a propertyA and a class TypeB, extending TypeA, with a propertyB:
{
  "$schema": "http://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "TypeA": {
      "properties": {
        "propertyA": {
          "type": "number"
        }
      },
      "required": [
        "propertyA"
      ]
    },
    "TypeB": {
      "allOf": [
        {
          "$ref": "#/$defs/TypeA"
        },
        {
          "type": "object",
          "properties": {
            "propertyB": {
              "type": "string"
            }
          },
          "required": [
            "propertyB"
          ]
        }
      ]
    }
  },
  "$ref": "#/$defs/TypeB"
}

This is a direct copy&paste from the above reference best practice document

Now let's use the check-jsonschema Python utility (using latest version 0.29.2 at time of writing):

$ check-jsonschema --schemafile test.schema.json test.json
ok -- validation done

Life is good as expected.

Now let's consider a variation of our document with a unexpected property. Let's call it test_unexpected.json :

{
  "propertyA": 2,
  "propertyB": "x",
  "unexpected": "foo"
}

Let's validate it:

$ check-jsonschema --schemafile test.schema.json test_unexpected.json
ok -- validation done

The success of test_unexpected is expected (pun intended). The reason is that by default a JSON schema allows additional properties. You need to use an explicit keyword in a JSON schema to disallow them: additionalProperties: false. That's one of the key thing I wanted for the PROJJSON schema. When there are non-mandatory members, I wanted to be able to catch typos, by rejecting additional properties. That way it would catch someone mispelling "datum_ensemble" as "datum_ensembble" , or putting a lowercase where an uppercase is expected, or forgetting a underscore, or whatever.
And that's where things are tricky. My findings is that inheritance using allOff is fine if used alone, additionalProperties: false is fine if used alone, but if you start combining them together, things break apart.

Let's see by modifying test.schema.json as test_add_properties_false.schema.json with

{
  "$schema": "http://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "TypeA": {
      "properties": {
        "propertyA": {
          "type": "number"
        }
      },
      "required": [
        "propertyA"
      ]
    },
    "TypeB": {
      "allOf": [
        {
          "$ref": "#/$defs/TypeA"
        },
        {
          "type": "object",
          "properties": {
            "propertyB": {
              "type": "string"
            }
          },
          "required": [
            "propertyB"
          ]
        }
      ],
      "additionalProperties": false
    }
  },
  "$ref": "#/$defs/TypeB"
}

Now let's validate the doc that has the unexpected member:

$ check-jsonschema --schemafile test_add_properties_false.schema.json test_unexpected.json 
Schema validation errors were encountered.
  test_unexpected.json::$: Additional properties are not allowed ('propertyA', 'propertyB', 'unexpected' were unexpected)

It gets rejected, but or a wrong reason. It also considers propertyA and propertyB to be unexpected.
OK, let's try with a valid instance:

$ check-jsonschema --schemafile test_add_properties_false.schema.json test.json 
Schema validation errors were encountered.
  test.json::$: Additional properties are not allowed ('propertyA', 'propertyB' were unexpected)

Same issue. That's not good. So this is not the way to go.

Let's experiment with another approach for test.json.schema. Let's call it test2.json.schema:

{
  "$schema": "http://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "TypeA": {
      "properties": {
        "propertyA": {
          "type": "number"
        }
      },
      "required": [
        "propertyA"
      ]
    },
    "TypeB": {
      "type": "object",
      "allOf": [{"$ref": "#/$defs/TypeA"}],
      "properties": {
        "propertyB": {
          "type": "string"
        }
      },
      "required": [
        "propertyB"
      ]
    }
  },
  "$ref": "#/$defs/TypeB"
}

So basically we move the allOf constraint inside typeB

Valid documents still validate:

$ check-jsonschema --schemafile test2.schema.json  test.json
ok -- validation done
$ check-jsonschema --schemafile test2.schema.json test_unexpected.json
ok -- validation done

Now let's add additionalProperties:false in it to create a test2_add_properties_false.schema.json schema:

{
  "$schema": "http://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "TypeA": {
      "properties": {
        "propertyA": {
          "type": "number"
        }
      },
      "required": [
        "propertyA"
      ]
    },
    "TypeB": {
      "type": "object",
      "allOf": [{"$ref": "#/$defs/TypeA"}],
      "properties": {
        "propertyB": {
          "type": "string"
        }
      },
      "required": [
        "propertyB"
      ],
      "additionalProperties": false
    }
  },
  "$ref": "#/$defs/TypeB"
}

Let' s try it:

$ check-jsonschema --schemafile test2_add_properties_false.schema.json test.json
Schema validation errors were encountered.
  test.json::$: Additional properties are not allowed ('propertyA' was unexpected)
$ check-jsonschema --schemafile test2_add_properties_false.schema.json test_unexpected.json
Schema validation errors were encountered.
  test_unexpected.json::$: Additional properties are not allowed ('propertyA', 'unexpected' were unexpected)

So things are slightly better in which it doesn't reject anymore propertyB, but it rejects the inherited propertyA.

This is why in projjsonschema, I found out that you have to re-mention inherits member of upper classes. So let's try that and create test_redefine_everything.schema.json with:

{
  "$schema": "http://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "TypeA": {
      "properties": {
        "propertyA": {
          "type": "number"
        }
      },
      "required": [
        "propertyA"
      ]
    },
    "TypeB": {
      "type": "object",
      "allOf": [{"$ref": "#/$defs/TypeA"}],
      "properties": {
        "propertyA": {},
        "propertyB": {
          "type": "string"
        }
      },
      "required": [
        "propertyA",
        "propertyB"
      ],
      "additionalProperties": false
    }
  },
  "$ref": "#/$defs/TypeB"
}

And now we get the expected results!

$ check-jsonschema --schemafile test_redefine_everything.schema.json test.json
ok -- validation done
$ check-jsonschema --schemafile test_redefine_everything.schema.json test_unexpected.json
Schema validation errors were encountered.
  test_unexpected.json::$: Additional properties are not allowed ('unexpected' was unexpected)

Which correlates with the practice in projjson.schema.json, like the following one where geodetic_crs has to mention the members inherited from object_usage:

    "geodetic_crs": {
      "type": "object",
      "properties": {
        "type": { "type": "string", "enum": ["GeodeticCRS", "GeographicCRS"] },
        "name": { "type": "string" },
        "datum": {
            "oneOf": [
                { "$ref": "#/definitions/geodetic_reference_frame" },
                { "$ref": "#/definitions/dynamic_geodetic_reference_frame" }
            ]
        },
        "datum_ensemble": { "$ref": "#/definitions/datum_ensemble" },
        "coordinate_system": { "$ref": "#/definitions/coordinate_system" },
        "deformation_models": {
          "type": "array",
          "items": { "$ref": "#/definitions/deformation_model" }
        },
        "$schema" : {},
        "scope": {},
        "area": {},
        "bbox": {},
        "vertical_extent": {},
        "temporal_extent": {},
        "usages": {},
        "remarks": {},
        "id": {}, "ids": {}
      },
      "required" : [ "name" ],
      "description": "One and only one of datum and datum_ensemble must be provided",
      "allOf": [
        { "$ref": "#/definitions/object_usage" },
        { "$ref": "#/definitions/one_and_only_one_of_datum_or_datum_ensemble" }
      ],
      "additionalProperties": false
    },

This is why I say that inheritance in JSON schema is not a prime concept.

But... all of that was true in the draf-07 era. draft 2019-09 brings a new keyword, "unevaluatedProperties", which is a kind of "additionalProperties", but that works with inheritance: https://json-schema.org/understanding-json-schema/reference/object#unevaluatedproperties

So let's now slightly edit the original test.schema.json as test_unevaluatedProperties.schema.json by adding a unevaluatedProperties: false member in it:

{
  "$schema": "http://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "TypeA": {
      "properties": {
        "propertyA": {
          "type": "number"
        }
      },
      "required": [
        "propertyA"
      ]
    },
    "TypeB": {
      "allOf": [
        {
          "$ref": "#/$defs/TypeA"
        },
        {
          "type": "object",
          "properties": {
            "propertyB": {
              "type": "string"
            }
          },
          "required": [
            "propertyB"
          ]
        }
      ],
      "unevaluatedProperties": false
    }
  },
  "$ref": "#/$defs/TypeB"
}

And now:

$ check-jsonschema --schemafile test_unevaluatedProperties.schema.json test_une
test_unevaluatedProperties.schema.json  test_unexpected.json                    

$ check-jsonschema --schemafile test_unevaluatedProperties.schema.json test_unexpected.json 
Schema validation errors were encountered.
  test_unexpected.json::$: Unevaluated properties are not allowed ('unexpected' was unexpected)

Yes!!! Success. So the Best Practice document should be updated to mention that "unevaluatedProperties": false must be used with inheritance if using JSON Schema Draft >= 2019-09. And this is the key to simplify projjson.schema.json without all the tedious copy&paste bloat I had to do at the time of Draft 07 (PROJJSON was initially released in July 2019)

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Inizia dal paragrafo 7.3.3.4, “Inheritance”, del documento OGC Best Practice for UML to JSON encoding rules e confronta le relative indicazioni per Draft 07 con l’esempio test_unevaluatedProperties.schema.json. Aggiorna le indicazioni per JSON Schema Draft 2019-09 e versioni successive, spiegando il rifiuto previsto dei membri imprevisti e preservando al contempo le proprietà ereditate.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
json
Ambito
documentation
Tipo di issue
Documentazione
Difficoltà
3/5
Tempo stimato
1-2 giorni
Stato di attività
Ferma
Chiarezza
Specificata chiaramente
Idoneità per principianti
45/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.