Repository: fastify/fast-json-stringify (JavaScript)

Issue:
allOf does not support $ref
<!--
Before you submit an issue we recommend you visit [Fastify Help](https://github.com/fastify/help) and ask any questions you have or mention any problems you've had getting started with Fastify.

**Please read this entire template before posting any issue. If you ignore these instructions
and post an issue here that does not follow the instructions, your issue might be closed,
locked, and assigned the `missing discussion` label.**
-->

## 🐛 Bug Report

With `allOf`, a schema that is referenced via `$ref` gets stripped out of the stringified output. 

## To Reproduce

Steps to reproduce the behavior:

```js
const fastJson = require('fast-json-stringify');
const stringify = fastJson({
  type: 'object',
  definitions: {
    names: {
      type: 'object',
      properties: {
        firstName: { type: 'string' },
        lastName: { type: 'string' },
      },
    },
  },
  properties: {
    user: {
      type: 'object',
      allOf: [{ $ref: '#/definitions/names' }],
    },
  },
});

console.log(stringify({ user: { firstName: 'john', lastName: 'doe' } })); // {"user":{}}
```

## Expected behavior

That `firstName` and `lastName` would be included in the output. Compare with the following where `$ref` is not used:

```js
const fastJson = require('fast-json-stringify');
const stringify = fastJson({
  type: 'object',
  definitions: {
    names: {
      type: 'object',
      properties: {
        firstName: { type: 'string' },
        lastName: { type: 'string' },
      },
    },
  },
  properties: {
    user: {
      type: 'object',
      allOf: [
        {
          type: 'object',
          properties: {
            firstName: { type: 'string' },
            lastName: { type: 'string' },
          },
        },
      ],
    },
  },
});

console.log(stringify({ user: { firstName: 'john', lastName: 'doe' } })); 
// {"user":{"firstName":"john","lastName":"doe"}}
```

## Your Environment

- *node version*: 12.6.1
- *fast-json-stringify*: 2.0.0
- *os*: Linux



Discussion:
Would you like to send a Pull Request to address this issue? Remember to add unit tests.
Sure thing. Would you mind jump starting me in the right direction, the
codebase looked quite cryptic at first glance earlier today.

On Thu, 14 May 2020, 8:50 pm Matteo Collina, <notifications@github.com>
wrote:

> Would you like to send a Pull Request to address this issue? Remember to
> add unit tests.
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/fastify/fast-json-stringify/issues/223#issuecomment-628823119>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAC3O3UF7KAVIYAAVKWEGF3RRQ4OTANCNFSM4NA37GNA>
> .
>

We should do something similar to https://github.com/fastify/fast-json-stringify/blob/c4fbf833cacf4a97a18f2d3b2217d9a4188dc030/index.js#L632 in https://github.com/fastify/fast-json-stringify/blob/c4fbf833cacf4a97a18f2d3b2217d9a4188dc030/index.js#L736.

I think we should pass through the definitions.
I'm experiencing a similar issue that is probably related:

In my schema's "response":

```
    200: {
      type: 'array',
      items: {
        anyOf: [
          { $ref: 'someref1' },
          { $ref: 'someref2' }
        ]
      }
    }
```
This does not work.

I've submitted [a PR](https://github.com/fastify/fast-json-stringify/pull/224) for this.

@ahgentil, I haven't tested it with actual code but it looks like `$ref` in `anyOf` does work. This test is already on master and passing: https://github.com/fastify/fast-json-stringify/blob/c4fbf833cacf4a97a18f2d3b2217d9a4188dc030/test/anyof.test.js#L252-L295

Consider creating a new issue with some code that can fully reproduce your case. 



Patch:
diff --git a/index.js b/index.js
index 5423cb69..a97c3fc5 100644
--- a/index.js
+++ b/index.js
@@ -627,6 +627,10 @@ function sanitizeKey (key) {
 }
 
 function buildCode (schema, code, laterCode, name, externalSchema, fullSchema) {
+  if (schema.$ref) {
+    schema = refFinder(schema.$ref, fullSchema, externalSchema)
+  }
+
   Object.keys(schema.properties || {}).forEach((key, i, a) => {
     if (schema.properties[key].$ref) {
       // if the schema object is deep in the tree, we must resolve the ref in the parent scope


Test patch:
diff --git a/test/allof.test.js b/test/allof.test.js
index 8d01081d..fdc76bfe 100644
--- a/test/allof.test.js
+++ b/test/allof.test.js
@@ -184,3 +184,200 @@ test('object with nested allOfs', (t) => {
     t.fail()
   }
 })
+
+test('object with $ref in allOf', (t) => {
+  t.plan(1)
+
+  const schema = {
+    title: 'object with $ref in allOf',
+    type: 'object',
+    definitions: {
+      id1: {
+        type: 'object',
+        properties: {
+          id1: {
+            type: 'integer'
+          }
+        }
+      }
+    },
+    allOf: [
+      {
+        $ref: '#/definitions/id1'
+      }
+    ]
+  }
+
+  try {
+    const stringify = build(schema)
+    try {
+      const value = stringify({
+        id1: 1,
+        id2: 2 // extra prop shouldn't be in result
+      })
+      t.is(value, '{"id1":1}')
+    } catch (e) {
+      t.fail()
+    }
+  } catch (e) {
+    t.fail()
+  }
+})
+
+test('object with $ref and other object in allOf', (t) => {
+  t.plan(1)
+
+  const schema = {
+    title: 'object with $ref in allOf',
+    type: 'object',
+    definitions: {
+      id1: {
+        type: 'object',
+        properties: {
+          id1: {
+            type: 'integer'
+          }
+        }
+      }
+    },
+    allOf: [
+      {
+        $ref: '#/definitions/id1'
+      },
+      {
+        type: 'object',
+        properties: {
+          id2: {
+            type: 'integer'
+          }
+        }
+      }
+    ]
+  }
+
+  try {
+    const stringify = build(schema)
+    try {
+      const value = stringify({
+        id1: 1,
+        id2: 2,
+        id3: 3 // extra prop shouldn't be in result
+      })
+      t.is(value, '{"id1":1,"id2":2}')
+    } catch (e) {
+      t.fail()
+    }
+  } catch (e) {
+    t.fail()
+  }
+})
+
+test('object with multiple $refs in allOf', (t) => {
+  t.plan(1)
+
+  const schema = {
+    title: 'object with $ref in allOf',
+    type: 'object',
+    definitions: {
+      id1: {
+        type: 'object',
+        properties: {
+          id1: {
+            type: 'integer'
+          }
+        }
+      },
+      id2: {
+        type: 'object',
+        properties: {
+          id2: {
+            type: 'integer'
+          }
+        }
+      }
+    },
+    allOf: [
+      {
+        $ref: '#/definitions/id1'
+      },
+      {
+        $ref: '#/definitions/id2'
+      }
+    ]
+  }
+
+  try {
+    const stringify = build(schema)
+    try {
+      const value = stringify({
+        id1: 1,
+        id2: 2,
+        id3: 3 // extra prop shouldn't be in result
+      })
+      t.is(value, '{"id1":1,"id2":2}')
+    } catch (e) {
+      t.fail()
+    }
+  } catch (e) {
+    t.fail()
+  }
+})
+
+test('object with external $refs in allOf', (t) => {
+  t.plan(1)
+
+  const externalSchema = {
+    first: {
+      definitions: {
+        id1: {
+          type: 'object',
+          properties: {
+            id1: {
+              type: 'integer'
+            }
+          }
+        }
+      }
+    },
+    second: {
+      id2: {
+        $id: '#id2',
+        type: 'object',
+        properties: {
+          id2: {
+            type: 'integer'
+          }
+        }
+      }
+    }
+  }
+
+  const schema = {
+    title: 'object with $ref in allOf',
+    type: 'object',
+    allOf: [
+      {
+        $ref: 'first#/definitions/id1'
+      },
+      {
+        $ref: 'second#id2'
+      }
+    ]
+  }
+
+  try {
+    const stringify = build(schema, { schema: externalSchema })
+    try {
+      const value = stringify({
+        id1: 1,
+        id2: 2,
+        id3: 3 // extra prop shouldn't be in result
+      })
+      t.is(value, '{"id1":1,"id2":2}')
+    } catch (e) {
+      t.fail()
+    }
+  } catch (e) {
+    t.fail()
+  }
+})