Repository: fastify/fastify (JavaScript)

Issue:
Json schema $ref id
## 🐛 Bug Report

Hi, testing the (fluent) json-schema I found that the [id-with-ref](https://json-schema.org/understanding-json-schema/structuring.html#using-id-with-ref) function is broken and return on start:

> { [Error: can't resolve reference #address from id #]
  message: 'can\'t resolve reference #address from id #',
  missingRef: '#address',
  missingSchema: '' }

## To Reproduce

```js
const fastify = require('./fastify')({ logger: { level: 'debug' } })

const body = {
  '$schema': 'http://json-schema.org/draft-07/schema#',
  'definitions': {
    '#address': {
      'type': 'object',
      '$id': '#address',
      'properties': {
        'city': {
          'type': 'string'
        },
        'zipcode': {
          'type': 'string'
        }
      }
    }
  },
  'type': 'object',
  '$id': 'http://foo/user',
  'title': 'My First Fluent JSON Schema',
  'description': 'A simple user',
  'properties': {
    'email': {
      'type': 'string',
      'format': 'email'
    },
    'address': {
      '$ref': '#address'
    }
  },
  'required': [
    'email'
  ]
}

fastify.post('/', { schema: { body } }, (request, reply) => { reply.send('good') })
fastify.listen(3000).catch(console.log)
```

## Expected behavior

The JSON schema is a valid one and use the `$ref: '#id'` function so the error doen't have to be thrown and the `#address` should be resolved.

## Your Environment

- *node version*: 10
- *fastify version*: >= 2.0.0-rc.4 `master branch`
- *os*: Windows

I would like to work on it 😃
I found that the problem is due to this line for the issue #1043  https://github.com/fastify/fastify/blob/310cb60b439fb363b28d42eb2e0f351447c7f11b/lib/schemas.js#L83
-if I comment it the example works fine, but the test fails- 



Discussion:
Go for it! I think we removed that property for some reason, can you investigate?
I fixed the problem, PR is coming.

I'll try to explain the problem as best as I can, because I think could be the start of a discussion:

1. the `schemas.js#traverse` is executed and all the field with the value that ends with `#` are changed to the shared-schema corresponding
1. all the fields now are pointing to the same object with the `$id` set
1. that `$id` is a fake one, is not regarding the json-schema standard: is only to reference a shared schema with the pattern `<shared-schema-$id>#`
1. so this `$id` is removed, otherwise, the `AJV.checkUnique` will throw an exception if it finds multiple equals `$id`

So, this deletion may be done only for those `$id` that are not standard, in other word only for the `$id` used in the shared schema (the PR implement this behaviour)

⚠ Warning:
`$id` and `$ref`, looking the draft07, could be set with a [JSON Pointer](https://www.ietf.org/archive/id/draft-handrews-relative-json-pointer-01.txt). [This is even discouraged](https://tools.ietf.org/html/draft-handrews-json-schema-01#section-8.2.3):

> Using JSON Pointer fragments requires knowledge of the structure of
   the schema.  When writing schema documents with the intention to
   provide re-usable schemas, it may be preferable to use a plain name
   fragment

The warning is due the pattern that JSON pointers could have:

                   "0/objects"                 true
                   "1/nested/objects"          true
                   "2/foo/0"                   "bar"
                   "0#"                        "nested"
                   "1#"                        "highly"

The lastest would match the fastify `<shared-schema-$id>#` and would break the startup.
So, at the actual state, we need to know that if some one use JSON Pointers in `$id` or `$ref`, will have issues.

@Eomm do we support "plain name fragments"?
Sorry, false alarm 😅

I would like to explain for the future: reading the json-schema draft 07 the `$id` MUST be a string, and MUST represent a valid URI-reference. 
This value defines a URI for the json-schema itself.

So, either the `$id` and the `$ref` field must have a URI value.

An URI could be like:

| Format | Regexp | Example | Note
|---|---|---|---|
| Plain name fragment | `^#[A-Za-z]{1}[\w-:.]{0,}$` | `#my-id` | In the root schema there is a `$id=#my-id`
| JSON Pointer fragment | [here](https://tools.ietf.org/html/rfc6901#section-6) | `#/definitions/address` | In the `definitions` there is a field `address: { type: 'object' ....etc...}`
| JSON Relative Pointer | [here](https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-6) | `1#` | ❌ WRONG this format can't be used like an URI ❌

In the json-relative-pointer standard is written:

> Unlike a JSON Pointer, a Relative JSON Pointer can not be used in a URI fragment identifier.

And also in the json-schema draft is written:

> The effect of using a fragment in "$id" that isn't blank or doesn't follow the plain name syntax is undefined

So, I don't know why the json-schema site show the link to the json-relative-pointer in the home page if they don't want to use it 😅

Anyway this is perfect because we will never have collision with the shared-schema feature.

Open an issue on the website, I think they should be fixing it.
I don't understand what would be the solution for us.
@mcollina @Eomm JSON Relative Pointer is a proposed standard maintained by the JSON Schema project (and it will be re-submitted when we re-submit the others- they are all temporarily expired for reasons not worth going into here).

In particular, JSON Relative Pointer is used by JSON Hyper-Schema.  It is not used in the URI Fragment syntax for the `application/schema+json` or `application/schema-instance+json` media types, but it is used elsewhere in the project.

As @Eomm notes, the specification for `$id` is quite clear on this already.  JSON Pointer and JSON Relative Pointer are separate specifications, and only JSON Pointer and plain name are allowed.

In the original example posted above, `'$id': '#address'` is a valid plain name fragment identifier.  I personally find naming the definition `#address` as well somewhat confusing (because then it looks like they have to match, which they don't), but it's a legal definition name.
Thanks for joining the discussion @handrews and for the explanation!



Patch:
diff --git a/fastify.js b/fastify.js
index ac8cee64fb5..fca0918ce33 100644
--- a/fastify.js
+++ b/fastify.js
@@ -800,10 +800,10 @@ function build (options) {
     instance[kChildren].forEach(child => _addHook(child, name, fn))
   }
 
-  function addSchema (name, schema) {
+  function addSchema (schema) {
     throwIfAlreadyStarted('Cannot call "addSchema" when fastify instance is already started!')
-    this[kSchemas].add(name, schema)
-    this[kChildren].forEach(child => child.addSchema(name, schema))
+    this[kSchemas].add(schema)
+    this[kChildren].forEach(child => child.addSchema(schema))
     return this
   }
 
diff --git a/lib/schemas.js b/lib/schemas.js
index 3d9838e7973..b4c1939b2ed 100644
--- a/lib/schemas.js
+++ b/lib/schemas.js
@@ -8,6 +8,8 @@ const {
   }
 } = require('./errors')
 
+const URI_NAME_FRAGMENT = /^#[A-Za-z]{1}[\w-:.]{0,}$/
+
 function Schemas () {
   this.store = {}
 }
@@ -80,7 +82,9 @@ Schemas.prototype.traverse = function (schema) {
 
 Schemas.prototype.cleanId = function (schema) {
   for (var key in schema) {
-    if (key === '$id') delete schema[key]
+    if (key === '$id' && !URI_NAME_FRAGMENT.test(schema[key])) {
+      delete schema[key]
+    }
     if (schema[key] !== null && typeof schema[key] === 'object') {
       this.cleanId(schema[key])
     }


Test patch:
diff --git a/test/shared-schemas.test.js b/test/shared-schemas.test.js
index bf774c8204d..166e87c3b5a 100644
--- a/test/shared-schemas.test.js
+++ b/test/shared-schemas.test.js
@@ -441,6 +441,65 @@ test('Use the same schema id in diferent places', t => {
   })
 })
 
+test('Use shared schema and $ref with $id', t => {
+  t.plan(2)
+  const fastify = Fastify()
+
+  fastify.addSchema({
+    $id: 'test',
+    type: 'object',
+    properties: {
+      id: { type: 'number' }
+    }
+  })
+
+  const body = {
+    $schema: 'http://json-schema.org/draft-07/schema#',
+    $id: 'http://foo/user',
+    type: 'object',
+    definitions: {
+      address: {
+        $id: '#address',
+        type: 'object',
+        properties: {
+          city: { 'type': 'string' }
+        }
+      }
+    },
+    properties: {
+      test: 'test#',
+      address: { $ref: '#address' }
+    }
+  }
+
+  fastify.route({
+    method: 'POST',
+    url: '/',
+    schema: {
+      body,
+      response: {
+        200: 'test#'
+      }
+    },
+    handler: (req, reply) => {
+      reply.send(req.body.test)
+    }
+  })
+
+  const id = Date.now()
+  fastify.inject({
+    method: 'POST',
+    url: '/',
+    payload: {
+      address: { city: 'New Node' },
+      test: { id }
+    }
+  }, (err, res) => {
+    t.error(err)
+    t.deepEqual(JSON.parse(res.payload), { id })
+  })
+})
+
 // https://github.com/fastify/fastify/issues/1043
 test('The schema resolver should clean the $id key before passing it to the compiler', t => {
   t.plan(1)