w3c / wot-thing-description

Web of Things (WoT) Thing Description
http://w3c.github.io/wot-thing-description/
Other
131 stars 63 forks source link

Consider to cross reference JSON Schema definitions (external or internal) #604

Open jmcanterafonseca opened 5 years ago

jmcanterafonseca commented 5 years ago

TL;DR Enable "$ref" in the Data Schema Vocabulary Definitions of a TD. It will make things clearer and easier

JSON Schema content could be reused from existing API descriptions or defined in the TD itself.
What makes me wonder if TD could have just a definitions section the same as JSON Schema and inside input, output, subscription, etc. just reuse definitions from the definitions section ... (instead of inlining schemas which make things a bit weird). it would save a lot of JSON Schema specification verbosity and will make the TD much more clearer, IMHO.

See example 1 (external JSON Schema): ($ref would be resolved taking into account the base``) See example 2 (from adefinitions` section inside the TD)

Example 1:

"base": "https://servient.example.com/",
"events": {
        "temperatureValueForT2T": {
            "description": "Provides periodic temperature values as event affordance.",
            "subscription": {
                "input": {
                    "$ref": "schema.json#/definitions/inputSubscription"
                }
            },
            "cancellation": {
                "input": {
                   "$ref": "schema.json#/definitions/inputCancellation"
                   }}}

Example 2:

{
  "@context": "https://www.w3.org/2019/wot/td/v1",
  "id": "urn:dev:wot:com:example:servient:lamp",
  "name": "MyLampThing",
  "definitions": {
    "MyProperty": {
       "type": "object",
      "properties": {
        "a": {
          "type": "string"
        }
     }
  },
  "securityDefinitions": {
    "basic_sc": {
      "scheme": "basic",
      "in": "header"
    }
  },
  "security": [
    "basic_sc"
  ],
  "properties": {
    "status": {
      "$ref": "#/definitions/MyProperty"
      },
      "forms": [
        {
          "href": "https://mylamp.example.com/status"
        }
      ]
    }
  },
  "actions": {
    "toggle": {
      "forms": [
        {
          "href": "https://mylamp.example.com/toggle"
        }
      ]
    }
  },
  "events": {
    "overheating": {
      "data": {
        "type": "string"
      },
      "forms": [
        {
          "href": "https://mylamp.example.com/oh",
          "subprotocol": "longpoll"
        }
      ]
    }
  }
}
egekorkan commented 5 years ago

Actually, there was a similar discussion before. I think example 1 can be already done via JSON-LD references but this wouldn't be JSON Schema compatible. Your example already works if you copy the contents of input into a JSON Schema processor. Example 2 is a more complicated case. A normal JSON Schema processor cannot understand this since the words properties and then status is not JSON Schema keywords, so a JSON Schema processor will not look inside them. The solution would be to create a JSON Schema instance for each place that contains a JSON Schema (input, output, subscription, etc.) and then always integrate the definitions into it. So one would need to generate as a preprocessing step of the TD:

"overheating": {
  "definitions": {
    "MyProperty": {
       "type": "object",
      "properties": {
        "a": {
          "type": "string"
        }
     }
  },
  "data":{ "$ref": "#/definitions/MyProperty" },
  "forms": [{ "href": "https://mylamp.example.com/status"  } ]
}

I think this would create a useful but complicated way of specifying payloads. Also I don't remember seeing something like this in Plugfests. Additionally, this would work for input, output, subscription, data, cancellation but not for properties (affordance) since the JSON Schema is not separated.

It could be useful to look at how ajv deals with something similar: https://github.com/epoberezkin/ajv-merge-patch

handrews commented 4 years ago

@egekorkan I strongly recommend against the $merge keyword that ajv uses. After a discussion spanning six drafts, two sets of spec editors, two github repositories, upwards of 10 github issues, and well over 500 comments on those issues (over 200 on the final consolidated issue alone), we (the JSON Schema organization) decided to reject this proposal in favor of several others that, together, cover the various known use cases.

I can address this in more detail if/when the group revisits JSON Schema stuff.

egekorkan commented 4 years ago

Thank you @handrews , it was just a suggestion from reading online and I didn't have a strong opinion about it. The group will revisit the JSON Schema for somehow compacting the schemas that are used multiple times in a TD. I am not able to find the presentation but it was presented by @sebastiankb in the W3C TPAC 2019. The slot is this. @sebastiankb could you upload your presentation?

handrews commented 4 years ago

The group will revisit the JSON Schema for somehow compacting the schemas that are used multiple times in a TD.

It's been quite some time since I looked at the TD. Is there no place to store and $ref re-usable schemas?

egekorkan commented 4 years ago

There is no defined place to store schemas to be re-used. As in example 2 above, one can define a definitions object but not every TD or JSON Schema processor will understand that.

TuranElchuev commented 3 years ago

Here is a suggested simple way to solve the issue:

  1. Add e.g. a "dataSchemas" map into the root of a TD, which includes reusable schema objects of type TD "DataSchema".
  2. In a DataSchema instance (Property, Action input/output, Event data, etc.):
    • use e.g. a "schema" key to refer to a reusable schema by its name;
    • use other properties to extend the reusable schema and/or override its properties.
  3. Resolve "schema" references using a custom algorithm.

Example TD:

{
    ...
    "properties": {
        "temperature": {
            ...
            "schema": "temperatureValue",
            "readOnly": true (EXTEND)
        }
    },
    "actions": {
        "setOverheatThreshold": {
            ...
            "input": {
                "schema": "temperatureValue",
                "minimum": 20 (OVERRIDE)
            }
        }
    },
    ...
    "dataSchemas": {
        "temperatureValue": {
            "type": "number",
            "minimum": -50,
            "maximum": 50,
            "unit": "Celsius"
        }
    }
}

Example TD after resolution:

{
    ...
    "properties": {
        "temperature": {
            ...
            "type": "number",
            "minimum": -50,
            "maximum": 50,
            "unit": "Celsius",
            "readOnly": true (EXTED)
        }
    },
    "actions": {
        "setOverheatThreshold": {
            ...
            "input": {
                "type": "number",
                "minimum": 20, (OVERRIDE)
                "maximum": 50,
                "unit": "Celsius"
            }
        }
    },
    ...
}

Resolution algorithm:

function resolveReusableSchemas(td){

    if(!td.dataSchemas){
        return td;
    }

    if(td.properties){
        for (let key in td.properties){
            resolveDataSchema(td.properties[key]);
        }
    }     
    if(td.actions){
        for (let key in td.actions){
            resolveDataSchema(td.actions[key].input);
            resolveDataSchema(td.actions[key].output);
        }
    }     
    if(td.events){
        for (let key in td.events){
            resolveDataSchema(td.events[key].data);
            resolveDataSchema(td.events[key].subscription);
            resolveDataSchema(td.events[key].cancellation);
        }
    }

    delete td.dataSchemas; // optional

    return td;
}

function resolveDataSchema(dataSchemaObj){
    if(dataSchemaObj && dataSchemaObj.schema){
        let reusableSchemaObj = td.dataSchemas[dataSchemaObj.schema];
        if(reusableSchemaObj){
            for (let key in reusableSchemaObj){

                // do not overwrite existing properties
                if(!(key in dataSchemaObj)){
                    dataSchemaObj[key] = reusableSchemaObj[key];
                }
            }
            // delete the 'schema' property after resolution
            delete dataSchemaObj.schema;
        }        
    }
}
handrews commented 3 years ago

@TuranElchuev

use other properties to extend the reusable schema and/or override its properties.

Keep in mind that JSON Schema is a constraint system. It is not possible to override constraints. You can add more constraints (and/or attach more annotations- which is more flexible as the consuming application can decide which annotations are significant), but you cannot lift them.

egekorkan commented 9 months ago

Should be evaluated together with #307