Repository: dubzzz/fast-check
Pull request #1327 (2021-01-06 19:46:07)

✨ Properly handle objects with symbols as keys in `fc.stringify`
<!-- Why is this PR for? -->
<!-- Describe the reason why you opened this PR or give a link towards the associated issue. -->

## In a nutshell

✔️ New feature
❌ Fix an issue
❌ Documentation improvement
❌ Other: *please explain*

(✔️: yes, ❌: no)

## Potential impacts

None

github-actions[bot]: Give a try to https://github.com/dubzzz/fast-check/pull/1327/commits/311ce5b3f8d365e8622165065120b4caa10c800d with:

```bash
yarn add https://pkg.csb.dev/dubzzz/fast-check/commit/311ce5b3/fast-check
npm i https://pkg.csb.dev/dubzzz/fast-check/commit/311ce5b3/fast-check
```

⚠️ Package might not be accessible yet. Wait for CodeSandbox.

⚠️ By running one of these commands you will install the package defined by the head of the PR. But, tests are run against the result of the merge of the PR, not against the head of the PR*. *_If needed, you can install the package used for the tests by manually retrieving and installing the artifact bundle stored into GitHub Actions._

Useful links: [Codeclimate](https://codeclimate.com/github/dubzzz/fast-check/pull/1327), [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1327), [CodeSandbox](https://ci.codesandbox.io/status/dubzzz/fast-check/pr/1327) and [GitHub Actions](https://github.com/dubzzz/fast-check/actions/runs/467003175).
codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/dubzzz/fast-check/pr/1327/builds/88226) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 311ce5b3f8d365e8622165065120b4caa10c800d:

|Sandbox| Source |
|--|--|
|[Vanilla](https://codesandbox.io/s/vanilla-1k1w1)| Configuration |
|[fast-check-examples](https://codesandbox.io/s/fast-check-examples-cokp3)| Configuration |

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1327?src=pr&el=h1) Report
> Merging [#1327](https://codecov.io/gh/dubzzz/fast-check/pull/1327?src=pr&el=desc) (311ce5b) into [master](https://codecov.io/gh/dubzzz/fast-check/commit/d5406c2eb1873dc2b627d2a1c969c746e6726f3f?el=desc) (d5406c2) will **decrease** coverage by `1.02%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/dubzzz/fast-check/pull/1327/graphs/tree.svg?width=650&height=150&src=pr&token=fRi2fJN730)](https://codecov.io/gh/dubzzz/fast-check/pull/1327?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1327      +/-   ##
==========================================
- Coverage   98.76%   97.74%   -1.03%     
==========================================
  Files          89       89              
  Lines        2914     2923       +9     
  Branches      540      544       +4     
==========================================
- Hits         2878     2857      -21     
- Misses         33       62      +29     
- Partials        3        4       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| e2e-tests | `?` | |
| e2e-tests-10.x-Linux | `?` | |
| e2e-tests-10.x-Windows | `?` | |
| e2e-tests-10.x-macOS | `?` | |
| e2e-tests-12.x-Linux | `?` | |
| e2e-tests-12.x-Windows | `?` | |
| e2e-tests-12.x-macOS | `?` | |
| e2e-tests-14.x-Linux | `?` | |
| e2e-tests-14.x-Windows | `?` | |
| e2e-tests-14.x-macOS | `?` | |
| unit-tests | `97.74% <100.00%> (+<0.01%)` | :arrow_up: |
| unit-tests-10.x-Linux | `?` | |
| unit-tests-12.x-Linux | `?` | |
| unit-tests-14.x-Linux | `97.74% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/dubzzz/fast-check/pull/1327?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/utils/stringify.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1327/diff?src=pr&el=tree#diff-c3JjL3V0aWxzL3N0cmluZ2lmeS50cw==) | `98.91% <100.00%> (+0.11%)` | :arrow_up: |
| [src/check/arbitrary/CloneArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1327/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9DbG9uZUFyYml0cmFyeS50cw==) | `86.04% <0.00%> (-13.96%)` | :arrow_down: |
| [src/check/arbitrary/TupleArbitrary.generic.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1327/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9UdXBsZUFyYml0cmFyeS5nZW5lcmljLnRz) | `88.09% <0.00%> (-11.91%)` | :arrow_down: |
| [src/check/arbitrary/ArrayArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1327/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9BcnJheUFyYml0cmFyeS50cw==) | `94.18% <0.00%> (-5.82%)` | :arrow_down: |
| [src/check/arbitrary/SubarrayArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1327/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9TdWJhcnJheUFyYml0cmFyeS50cw==) | `95.16% <0.00%> (-4.84%)` | :arrow_down: |
| [src/check/arbitrary/StringArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1327/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9TdHJpbmdBcmJpdHJhcnkudHM=) | `96.15% <0.00%> (-3.85%)` | :arrow_down: |
| [src/check/runner/RunnerIterator.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1327/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL3J1bm5lci9SdW5uZXJJdGVyYXRvci50cw==) | `96.29% <0.00%> (-3.71%)` | :arrow_down: |
| [src/check/arbitrary/ConstantArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1327/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9Db25zdGFudEFyYml0cmFyeS50cw==) | `96.55% <0.00%> (-3.45%)` | :arrow_down: |
| [src/check/runner/reporter/RunExecution.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1327/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL3J1bm5lci9yZXBvcnRlci9SdW5FeGVjdXRpb24udHM=) | `94.82% <0.00%> (-3.45%)` | :arrow_down: |
| [src/check/arbitrary/RecordArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1327/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9SZWNvcmRBcmJpdHJhcnkudHM=) | `97.36% <0.00%> (-2.64%)` | :arrow_down: |
| ... and [3 more](https://codecov.io/gh/dubzzz/fast-check/pull/1327/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1327?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1327?src=pr&el=footer). Last update [d5406c2...311ce5b](https://codecov.io/gh/dubzzz/fast-check/pull/1327?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

github-actions[bot]: Give a try to https://github.com/dubzzz/fast-check/pull/1327/commits/090e0c5791cea0902d2cca877efc407646899a81 with:

```bash
yarn add https://pkg.csb.dev/dubzzz/fast-check/commit/090e0c57/fast-check
npm i https://pkg.csb.dev/dubzzz/fast-check/commit/090e0c57/fast-check
```

⚠️ Package might not be accessible yet. Wait for CodeSandbox.

⚠️ By running one of these commands you will install the package defined by the head of the PR. But, tests are run against the result of the merge of the PR, not against the head of the PR*. *_If needed, you can install the package used for the tests by manually retrieving and installing the artifact bundle stored into GitHub Actions._

Useful links: [Codeclimate](https://codeclimate.com/github/dubzzz/fast-check/pull/1327), [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1327), [CodeSandbox](https://ci.codesandbox.io/status/dubzzz/fast-check/pr/1327) and [GitHub Actions](https://github.com/dubzzz/fast-check/actions/runs/467064696).
github-actions[bot]: Give a try to https://github.com/dubzzz/fast-check/pull/1327/commits/bd9101412ddca402548bae6c009a749818c6ef3c with:

```bash
yarn add https://pkg.csb.dev/dubzzz/fast-check/commit/bd910141/fast-check
npm i https://pkg.csb.dev/dubzzz/fast-check/commit/bd910141/fast-check
```

⚠️ Package might not be accessible yet. Wait for CodeSandbox.

⚠️ By running one of these commands you will install the package defined by the head of the PR. But, tests are run against the result of the merge of the PR, not against the head of the PR*. *_If needed, you can install the package used for the tests by manually retrieving and installing the artifact bundle stored into GitHub Actions._

Useful links: [Codeclimate](https://codeclimate.com/github/dubzzz/fast-check/pull/1327), [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1327), [CodeSandbox](https://ci.codesandbox.io/status/dubzzz/fast-check/pr/1327) and [GitHub Actions](https://github.com/dubzzz/fast-check/actions/runs/467071455).

Patch:
diff --git a/documentation/Arbitraries.md b/documentation/Arbitraries.md
index 7021c83c..9d050ee8 100644
--- a/documentation/Arbitraries.md
+++ b/documentation/Arbitraries.md
@@ -2059,10 +2059,14 @@ function buildCloneable(objectInstance) {
   // Rq: We do not handle deep objects in this snippet
   // But we will get another instance of objectInstance for each run
   // ie. objectInstanceRunA !== objectInstanceRunB while having isEqual(objectInstanceRunA, objectInstanceRunB)
-  const withCloneMethod = () => ({
-    ...objectInstance,
-    [fc.cloneMethod]: withCloneMethod,
-  });
+  const withCloneMethod = () => {
+    const clone = {...objectInstance};
+    Object.defineProperty(objectInstance, fc.cloneMethod, {
+      value: withCloneMethod,
+      enumerable: false,
+    });
+    return clone;
+  };
   return withCloneMethod();
 }
 // Use the arbitrary:
diff --git a/src/check/arbitrary/RecordArbitrary.ts b/src/check/arbitrary/RecordArbitrary.ts
index 1db9b7f1..636e61cc 100644
--- a/src/check/arbitrary/RecordArbitrary.ts
+++ b/src/check/arbitrary/RecordArbitrary.ts
@@ -11,6 +11,11 @@ export type RecordConstraints<T = unknown> =
   | {
       /**
        * List keys that should never be deleted.
+       *
+       * Remark:
+       * You might need to use an explicit typing in case you need to declare symbols as required (not needed when required keys are simple strings).
+       * With something like `{ requiredKeys: [mySymbol1, 'a'] as [typeof mySymbol1, 'a'] }` when both `mySymbol1` and `a` are required.
+       *
        * Warning: Cannot be used in conjunction with withDeletedKeys.
        */
       requiredKeys?: T[];
@@ -39,19 +44,35 @@ export type RecordValue<T, TConstraints = {}> = TConstraints extends { withDelet
   : T;
 
 /** @internal */
-function rawRecord<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }): Arbitrary<{ [K in keyof T]: T[K] }> {
-  const keys: Extract<keyof T, string>[] = [];
+type RecordKey<T> = Extract<keyof T, string | symbol>;
+
+/** @internal */
+function extractAllKeys<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }): RecordKey<T>[] {
+  const keys = Object.keys(recordModel) as RecordKey<T>[]; // Only enumerable own properties
+  const symbols = Object.getOwnPropertySymbols(recordModel) as RecordKey<T>[];
+  for (let index = 0; index !== symbols.length; ++index) {
+    const symbol = symbols[index];
+    const descriptor = Object.getOwnPropertyDescriptor(recordModel, symbol);
+    if (descriptor && descriptor.enumerable) {
+      keys.push(symbol);
+    }
+  }
+  return keys;
+}
+
+/** @internal */
+function rawRecord<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }): Arbitrary<T> {
+  const keys = extractAllKeys(recordModel);
   const arbs: Arbitrary<T[keyof T]>[] = [];
-  for (const k in recordModel) {
-    keys.push(k);
-    arbs.push(recordModel[k]);
+  for (let index = 0; index !== keys.length; ++index) {
+    arbs.push(recordModel[keys[index]]);
   }
   return genericTuple(arbs).map((gs: any[]) => {
-    const obj: { [key: string]: any } = {};
+    const obj: Record<RecordKey<T>, any> = {} as any;
     for (let idx = 0; idx !== keys.length; ++idx) {
       obj[keys[idx]] = gs[idx];
     }
-    return obj as { [K in keyof T]: T[K] };
+    return obj as T;
   });
 }
 
@@ -68,7 +89,7 @@ function rawRecord<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }): Arbitra
  *
  * @public
  */
-function record<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }): Arbitrary<RecordValue<{ [K in keyof T]: T[K] }>>;
+function record<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }): Arbitrary<RecordValue<T>>;
 /**
  * For records following the `recordModel` schema
  *
@@ -86,7 +107,7 @@ function record<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }): Arbitrary<
 function record<T, TConstraints extends RecordConstraints<keyof T>>(
   recordModel: { [K in keyof T]: Arbitrary<T[K]> },
   constraints: TConstraints
-): Arbitrary<RecordValue<{ [K in keyof T]: T[K] }, TConstraints>>;
+): Arbitrary<RecordValue<T, TConstraints>>;
 function record<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }, constraints?: RecordConstraints<keyof T>) {
   if (constraints == null) {
     return rawRecord(recordModel);
@@ -102,7 +123,7 @@ function record<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }, constraints
     return rawRecord(recordModel);
   }
 
-  const updatedRecordModel: { [key: string]: Arbitrary<{ value: T[keyof T] } | null> } = {};
+  const updatedRecordModel: { [K in keyof T]: Arbitrary<{ value: T[K] } | null> } = {} as any;
   const requiredKeys = ('requiredKeys' in constraints ? constraints.requiredKeys : undefined) || [];
 
   for (let idx = 0; idx !== requiredKeys.length; ++idx) {
@@ -111,15 +132,18 @@ function record<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }, constraints
     }
   }
 
-  for (const k in recordModel) {
+  const keys = extractAllKeys(recordModel);
+  for (let index = 0; index !== keys.length; ++index) {
+    const k = keys[index];
     const requiredArbitrary = recordModel[k].map((v) => ({ value: v }));
     if (requiredKeys.indexOf(k) !== -1) updatedRecordModel[k] = requiredArbitrary;
     else updatedRecordModel[k] = option(requiredArbitrary);
   }
-  return rawRecord(updatedRecordModel).map((obj) => {
-    const nobj: { [key: string]: T[keyof T] } = {};
-    for (const k in obj) {
-      if (obj[k] != null) {
+  return rawRecord(updatedRecordModel as any).map((obj: { [K in keyof T]: { value: T[keyof T] } | null }) => {
+    const nobj: { [K in keyof T]?: T[keyof T] } = {};
+    for (let index = 0; index !== keys.length; ++index) {
+      const k = keys[index];
+      if (obj[k] !== null) {
         nobj[k] = (obj[k] as { value: T[keyof T] }).value;
       }
     }
diff --git a/src/utils/stringify.ts b/src/utils/stringify.ts
index d2fe281b..ede27b51 100644
--- a/src/utils/stringify.ts
+++ b/src/utils/stringify.ts
@@ -65,18 +65,27 @@ export function stringifyInternal<Ts>(value: Ts, previousValues: any[]): string
         // Only return what would have been the default toString on Object
         return '[object Object]';
       }
-      const rawRepr =
-        '{' +
-        Object.keys(value)
-          .map(
-            (k) =>
-              `${k === '__proto__' ? '["__proto__"]' : JSON.stringify(k)}:${stringifyInternal(
-                (value as any)[k],
-                currentValues
-              )}`
-          )
-          .join(',') +
-        '}';
+
+      const mapper = (k: string | symbol) =>
+        `${
+          k === '__proto__'
+            ? '["__proto__"]'
+            : typeof k === 'symbol'
+            ? `[${stringifyInternal(k, currentValues)}]`
+            : JSON.stringify(k)
+        }:${stringifyInternal((value as any)[k], currentValues)}`;
+
+      const stringifiedProperties = [
+        ...Object.keys(value).map(mapper),
+        ...Object.getOwnPropertySymbols(value)
+          .filter((s) => {
+            const descriptor = Object.getOwnPropertyDescriptor(value, s);
+            return descriptor && descriptor.enumerable;
+          })
+          .map(mapper),
+      ];
+      const rawRepr = '{' + stringifiedProperties.join(',') + '}';
+
       if (Object.getPrototypeOf(value) === null) {
         return rawRepr === '{}' ? 'Object.create(null)' : `Object.assign(Object.create(null),${rawRepr})`;
       }


Test patch:
diff --git a/test/type/main.ts b/test/type/main.ts
index 70d49ca1..3c37794d 100644
--- a/test/type/main.ts
+++ b/test/type/main.ts
@@ -75,7 +75,21 @@ expectType<fc.Arbitrary<number | string>>()(fc.constantFrom(1, 2, 'hello'));
 expectType<fc.Arbitrary<1 | 2 | 'hello'>>()(fc.constantFrom(...([1, 2, 'hello'] as const)));
 
 // record arbitrary
+const mySymbol1 = Symbol('symbol1');
+const mySymbol2 = Symbol('symbol2');
 expectType<fc.Arbitrary<{ a: number; b: string }>>()(fc.record({ a: fc.nat(), b: fc.string() }));
+expectType<fc.Arbitrary<{ [mySymbol1]: number; [mySymbol2]: string }>>()(
+  fc.record({ [mySymbol1]: fc.nat(), [mySymbol2]: fc.string() })
+);
+// Related to https://github.com/microsoft/TypeScript/issues/27525:
+//expectType<fc.Arbitrary<{ [Symbol.iterator]: number; [mySymbol2]: string }>>()(
+//  fc.record({ [Symbol.iterator]: fc.nat(), [mySymbol2]: fc.string() })
+//);
+// Workaround for known symbols not defined as unique ones:
+//const symbolIterator: unique symbol = Symbol.iterator as any;
+//expectType<fc.Arbitrary<{ [symbolIterator]: number; [mySymbol2]: string }>>()(
+//  fc.record({ [symbolIterator]: fc.nat(), [mySymbol2]: fc.string() })
+//);
 expectType<fc.Arbitrary<{ a: number; b: string }>>()(fc.record({ a: fc.nat(), b: fc.string() }, {}));
 expectType<fc.Arbitrary<{ a: number; b: string }>>()(
   fc.record({ a: fc.nat(), b: fc.string() }, { withDeletedKeys: false })
@@ -92,6 +106,20 @@ expectType<fc.Arbitrary<{ a: number; b?: string }>>()(
 expectType<fc.Arbitrary<{ a: number; b?: string; c: string }>>()(
   fc.record({ a: fc.nat(), b: fc.string(), c: fc.string() }, { requiredKeys: ['a', 'c'] })
 );
+expectType<fc.Arbitrary<{ [mySymbol1]: number; [mySymbol2]?: string }>>()(
+  fc.record({ [mySymbol1]: fc.nat(), [mySymbol2]: fc.string() }, { requiredKeys: [mySymbol1] as [typeof mySymbol1] })
+);
+// Related to https://github.com/microsoft/TypeScript/issues/27525
+//expectType<fc.Arbitrary<{ [Symbol.iterator]: number; [mySymbol2]?: string }>>()(
+//  fc.record({ [Symbol.iterator]: fc.nat(), [mySymbol2]: fc.string() }, { requiredKeys: [Symbol.iterator] })
+//);
+// See workaround above
+expectType<fc.Arbitrary<{ [mySymbol1]: number; [mySymbol2]?: string; a: number; b?: string }>>()(
+  fc.record(
+    { [mySymbol1]: fc.nat(), [mySymbol2]: fc.string(), a: fc.nat(), b: fc.string() },
+    { requiredKeys: [mySymbol1, 'a'] as [typeof mySymbol1, 'a'] }
+  )
+);
 expectType<fc.Arbitrary<never>>()(
   // requiredKeys and withDeletedKeys cannot be used together
   // typings are not perfect but at least they build a value that cannot be used
diff --git a/test/unit/check/arbitrary/RecordArbitrary.spec.ts b/test/unit/check/arbitrary/RecordArbitrary.spec.ts
index 989606a5..7e739ec8 100644
--- a/test/unit/check/arbitrary/RecordArbitrary.spec.ts
+++ b/test/unit/check/arbitrary/RecordArbitrary.spec.ts
@@ -2,22 +2,25 @@ import * as prand from 'pure-rand';
 import * as fc from '../../../../lib/fast-check';
 
 import { constant } from '../../../../src/check/arbitrary/ConstantArbitrary';
+import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
 import { integer } from '../../../../src/check/arbitrary/IntegerArbitrary';
 import { record, RecordConstraints } from '../../../../src/check/arbitrary/RecordArbitrary';
 import { Random } from '../../../../src/random/generator/Random';
 
 import * as genericHelper from './generic/GenericArbitraryHelper';
 
-import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
+const keyArb: fc.Arbitrary<any> = fc
+  .tuple(fc.string(), fc.boolean())
+  .map(([name, symbol]) => (symbol ? Symbol.for(name) : name));
 
 describe('RecordArbitrary', () => {
   describe('record', () => {
     it('Should produce a record with missing keys', () =>
       fc.assert(
-        fc.property(fc.set(fc.string(), { minLength: 1 }), fc.nat(), fc.integer(), (keys, missingIdx, seed) => {
+        fc.property(fc.set(keyArb, { minLength: 1 }), fc.nat(), fc.integer(), (keys, missingIdx, seed) => {
           const mrng = new Random(prand.xorshift128plus(seed));
-          const recordModel: { [key: string]: Arbitrary<string> } = {};
-          for (const k of keys) recordModel[k] = constant(`_${k}_`);
+          const recordModel: Record<string | symbol, Arbitrary<any>> = {};
+          for (const k of keys) recordModel[k] = constant(k);
 
           const arb = record(recordModel, { withDeletedKeys: true });
           for (let idx = 0; idx != 1000; ++idx) {
@@ -29,29 +32,29 @@ describe('RecordArbitrary', () => {
       ));
     it('Should produce a record with present keys', () =>
       fc.assert(
-        fc.property(fc.set(fc.string(), { minLength: 1 }), fc.nat(), fc.integer(), (keys, missingIdx, seed) => {
+        fc.property(fc.set(keyArb, { minLength: 1 }), fc.nat(), fc.integer(), (keys, missingIdx, seed) => {
           const mrng = new Random(prand.xorshift128plus(seed));
-          const recordModel: { [key: string]: Arbitrary<string> } = {};
+          const recordModel: Record<string | symbol, Arbitrary<any>> = {};
           for (const k of keys) {
-            recordModel[k] = constant(`_${k}_`);
+            recordModel[k] = constant(k);
           }
 
           const arb = record(recordModel, { withDeletedKeys: true });
           for (let idx = 0; idx != 1000; ++idx) {
             const g = arb.generate(mrng).value;
-            if (g[keys[missingIdx % keys.length]] === `_${keys[missingIdx % keys.length]}_`) return true;
+            if (g[keys[missingIdx % keys.length]] === keys[missingIdx % keys.length]) return true;
           }
           return false;
         })
       ));
     it('Should reject configurations specifying non existing keys as required', () =>
       fc.assert(
-        fc.property(fc.set(fc.string(), { minLength: 1 }), fc.string(), (keys, requiredKey) => {
+        fc.property(fc.set(keyArb, { minLength: 1 }), keyArb, (keys, requiredKey) => {
           fc.pre(!keys.includes(requiredKey));
 
-          const recordModel: { [key: string]: Arbitrary<string> } = {};
+          const recordModel: Record<string | symbol, Arbitrary<any>> = {};
           for (const k of keys) {
-            recordModel[k] = constant(`_${k}_`);
+            recordModel[k] = constant(k);
           }
 
           expect(() =>
@@ -64,16 +67,16 @@ describe('RecordArbitrary', () => {
     it('Should reject configurations specifying both requiredKeys and withDeletedKeys (even undefined)', () =>
       fc.assert(
         fc.property(
-          fc.set(fc.record({ name: fc.string(), required: fc.boolean() }), {
+          fc.set(fc.record({ name: keyArb, required: fc.boolean() }), {
             minLength: 1,
             compare: (a, b) => a.name === b.name,
           }),
           fc.option(fc.constant(true), { nil: undefined }),
           fc.option(fc.boolean(), { nil: undefined }),
           (keys, withRequiredKeys, withDeletedKeys) => {
-            const recordModel: { [key: string]: Arbitrary<string> } = {};
+            const recordModel: Record<string | symbol, Arbitrary<any>> = {};
             for (const k of keys) {
-              recordModel[k.name] = constant(`_${k.name}_`);
+              recordModel[k.name] = constant(k.name);
             }
 
             expect(() =>
@@ -86,10 +89,10 @@ describe('RecordArbitrary', () => {
         )
       ));
 
-    type Meta = { key: string; valueStart: number; kept: boolean };
+    type Meta = { key: any; valueStart: number; kept: boolean };
     const metaArbitrary = fc.set(
       fc.record({
-        key: fc.string(),
+        key: keyArb,
         valueStart: fc.nat(1000),
         kept: fc.boolean(),
       }),
@@ -103,7 +106,7 @@ describe('RecordArbitrary', () => {
     describe('Given a custom record configuration', () => {
       genericHelper.isValidArbitrary(
         ([metas, constraints]: [Meta[], RecordConstraints<any>]) => {
-          const recordModel: { [key: string]: Arbitrary<number> } = {};
+          const recordModel: Record<string | symbol, Arbitrary<number>> = {};
           for (const m of metas) {
             recordModel[m.key] = integer(m.valueStart, m.valueStart + 10);
           }
@@ -122,7 +125,8 @@ describe('RecordArbitrary', () => {
               return [metas, constraintsMeta] as [Meta[], RecordConstraints];
             }),
           isValidValue: (r: { [key: string]: number }, [metas, constraints]: [Meta[], RecordConstraints]) => {
-            for (const k of Object.keys(r)) {
+            // Rq: getOwnPropertyNames will also get non enumerable properties, but there are none in our case
+            for (const k of [...Object.getOwnPropertyNames(r), ...Object.getOwnPropertySymbols(r)]) {
               // generated object should not have more keys
               if (metas.findIndex((m) => m.key === k) === -1) return false;
             }
@@ -148,10 +152,6 @@ describe('RecordArbitrary', () => {
               if (r[m.key] < m.valueStart) return false;
               if (r[m.key] > m.valueStart + 10) return false;
             }
-            for (const k in r) {
-              // all keys of the generated value comes from keys defined in metas
-              if (metas.find((m) => m.key === k) === undefined) return false;
-            }
             return true;
           },
         }
diff --git a/test/unit/utils/stringify.spec.ts b/test/unit/utils/stringify.spec.ts
index ca03c31e..ba0430eb 100644
--- a/test/unit/utils/stringify.spec.ts
+++ b/test/unit/utils/stringify.spec.ts
@@ -161,11 +161,74 @@ describe('stringify', () => {
     }
     expect(foundOne).toBe(true);
   });
+  it('Should be able to stringify Object', () => {
+    expect(stringify({ a: 1 })).toEqual('{"a":1}');
+    expect(stringify({ a: 1, b: 2 })).toEqual('{"a":1,"b":2}');
+    expect(stringify({ [Symbol.for('a')]: 1 })).toEqual('{[Symbol.for("a")]:1}');
+    expect(stringify({ a: 1, [Symbol.for('a')]: 1 })).toEqual('{"a":1,[Symbol.for("a")]:1}');
+    expect(stringify({ [Symbol.for('a')]: 1, a: 1 })).toEqual('{"a":1,[Symbol.for("a")]:1}');
+  });
+  it('Should be able to stringify Object but skip non enumerable properties', () => {
+    // At least for the moment we don't handle non enumerable properties
+    const obj: any = {};
+    Object.defineProperties(obj, {
+      a: { value: 1, enumerable: false },
+      b: { value: 1, enumerable: true },
+      [Symbol.for('a')]: { value: 1, enumerable: false },
+      [Symbol.for('b')]: { value: 1, enumerable: true },
+    });
+    expect(stringify(obj)).toEqual('{"b":1,[Symbol.for("b")]:1}');
+  });
+  it('Should be able to stringify instances of classes', () => {
+    class A {
+      public a: number;
+      constructor() {
+        this.a = 1;
+        (this as any)[Symbol.for('a')] = 2;
+      }
+      public ma() {
+        // no-op
+      }
+    }
+    expect(stringify(new A())).toEqual('{"a":1,[Symbol.for("a")]:2}');
+
+    class AA {
+      public a = 0;
+    }
+    expect(stringify(new AA())).toEqual('{"a":0}');
+  });
+  it('Should be able to stringify instances of classes inheriting from others', () => {
+    class A {
+      public a: number;
+      constructor() {
+        this.a = 1;
+        (this as any)[Symbol.for('a')] = 2;
+      }
+      public ma() {
+        // no-op
+      }
+    }
+    class B extends A {
+      public b;
+      constructor() {
+        super();
+        this.b = 3;
+        (this as any)[Symbol.for('b')] = 4;
+      }
+      public mb() {
+        // no-op
+      }
+    }
+    expect(stringify(new B())).toEqual('{"a":1,"b":3,[Symbol.for("a")]:2,[Symbol.for("b")]:4}');
+  });
   it('Should be able to stringify Object without prototype', () => {
     expect(stringify(Object.create(null))).toEqual('Object.create(null)');
     expect(stringify(Object.assign(Object.create(null), { a: 1 }))).toEqual(
       'Object.assign(Object.create(null),{"a":1})'
     );
+    expect(stringify(Object.assign(Object.create(null), { [Symbol.for('a')]: 1 }))).toEqual(
+      'Object.assign(Object.create(null),{[Symbol.for("a")]:1})'
+    );
   });
   it('Should be able to stringify Object with custom __proto__ value', () => {
     expect(stringify({ ['__proto__']: 1 })).toEqual('{["__proto__"]:1}');