Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,34 @@ You would usually not need this, but it could be useful if you use a custom `cwd
> [!NOTE]
> Setting restrictive permissions can cause problems if different users need to read the file. A common problem is a user running your tool with and without `sudo` and then not being able to access the config the second time.

#### deserializeComplexTypes

Type: `boolean`\
Default: `false`

Preserve non-JSON types like `Date` through serialization and deserialization.

When enabled, `Date` objects are tagged during serialization so they can be restored as proper `Date` instances when read back. This allows round-tripping of `Date` values.

```js
import Conf from 'conf';

const config = new Conf({
projectName: 'foo',
deserializeComplexTypes: true
});

config.set('timestamp', new Date());
console.log(config.get('timestamp') instanceof Date);
//=> true

config.set('nested', {createdAt: new Date()});
console.log(config.get('nested').createdAt instanceof Date);
//=> true
```

This option is ignored when custom `serialize` or `deserialize` functions are provided.

### Instance

You can use [dot-notation](https://github.com/sindresorhus/dot-prop) in a `key` to access nested properties.
Expand Down
45 changes: 45 additions & 0 deletions source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,45 @@ const checkValueType = (key: string, value: unknown): void => {
const INTERNAL_KEY = '__internal__';
const MIGRATION_KEY = `${INTERNAL_KEY}.migrations.version`;

const COMPLEX_TYPE_TAG = '$$type';
const COMPLEX_VALUE_TAG = '$$value';

/**
JSON replacer that tags `Date` objects for round-tripping.
Uses `this` binding from `JSON.stringify` to access the original value
(before `.toJSON()` converts it to a string).
*/
function complexTypeReplacer(this: Record<string, unknown>, key: string, value: unknown): unknown {
const rawValue = this[key];
if (rawValue instanceof Date) {
return {[COMPLEX_TYPE_TAG]: 'Date', [COMPLEX_VALUE_TAG]: rawValue.toISOString()};
}
Comment on lines +82 to +85
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rawValue.toISOString() will throw a RangeError for invalid Date instances (e.g. new Date('not-a-date')), so enabling deserializeComplexTypes will currently make .set() fail for invalid dates. Handle invalid dates explicitly (for example, tag them with a sentinel and revive to new Date(NaN), or store the original string and revive from that) so serialization doesn’t throw.

Copilot uses AI. Check for mistakes.

return value;
}

/**
JSON reviver that restores tagged objects back to their original types.
Only matches objects with exactly `$$type` and `$$value` keys.
*/
function complexTypeReviver(_key: string, value: unknown): unknown {
if (
value !== null
&& typeof value === 'object'
&& !Array.isArray(value)
&& COMPLEX_TYPE_TAG in value
&& COMPLEX_VALUE_TAG in value
&& Object.keys(value).length === 2
) {
const tagged = value as Record<string, unknown>;
if (tagged[COMPLEX_TYPE_TAG] === 'Date' && typeof tagged[COMPLEX_VALUE_TAG] === 'string') {
return new Date(tagged[COMPLEX_VALUE_TAG] as string);
}
}

return value;
}

export default class Conf<T extends Record<string, any> = Record<string, unknown>> implements Iterable<[keyof T, T[keyof T]]> {
readonly path: string;
readonly events: EventTarget;
Expand Down Expand Up @@ -845,12 +884,18 @@ export default class Conf<T extends Record<string, any> = Record<string, unknown
}

#configureSerialization(options: Partial<Options<T>>): void {
const hasCustomSerialization = Boolean(options.serialize ?? options.deserialize);

if (options.serialize) {
this._serialize = options.serialize;
} else if (options.deserializeComplexTypes && !hasCustomSerialization) {
this._serialize = value => JSON.stringify(value, complexTypeReplacer, '\t');
}

if (options.deserialize) {
this._deserialize = options.deserialize;
} else if (options.deserializeComplexTypes && !hasCustomSerialization) {
this._deserialize = value => JSON.parse(value, complexTypeReviver);
}
}

Expand Down
31 changes: 31 additions & 0 deletions source/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,37 @@ export type Options<T extends Record<string, unknown>> = {
@default 0o666
*/
readonly configFileMode?: number;

/**
Preserve non-JSON types like `Date` through serialization and deserialization.

When enabled, `Date` objects are tagged during serialization so they can be restored as proper `Date` instances when read back. This allows round-tripping of `Date` values.

Uses a tagged wrapper format in the JSON: `{"$$type": "Date", "$$value": "2024-01-01T00:00:00.000Z"}`.

Comment on lines +309 to +314
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs describe preserving “non-JSON types like Date”, but the current implementation/reviver only supports Date. Consider explicitly documenting that only Date is supported today (and potentially list future candidates) to avoid users assuming other complex types will also round-trip.

Suggested change
Preserve non-JSON types like `Date` through serialization and deserialization.
When enabled, `Date` objects are tagged during serialization so they can be restored as proper `Date` instances when read back. This allows round-tripping of `Date` values.
Uses a tagged wrapper format in the JSON: `{"$$type": "Date", "$$value": "2024-01-01T00:00:00.000Z"}`.
Preserve `Date` instances (and only `Date`) through serialization and deserialization.
When enabled, `Date` objects are tagged during serialization so they can be restored as proper `Date` instances when read back. This allows round-tripping of `Date` values.
Uses a tagged wrapper format in the JSON for `Date` values: `{"$$type": "Date", "$$value": "2024-01-01T00:00:00.000Z"}`.
Other non-JSON types (such as `Map`, `Set`, or `BigInt`) are not currently preserved by this option and will not round-trip as their original types.

Copilot uses AI. Check for mistakes.
This option is ignored when custom `serialize` or `deserialize` functions are provided.

@default false

@example
```
import Conf from 'conf';

const config = new Conf({
projectName: 'foo',
deserializeComplexTypes: true
});

config.set('timestamp', new Date());
config.get('timestamp') instanceof Date;
//=> true

config.set('nested', {createdAt: new Date()});
config.get('nested').createdAt instanceof Date;
//=> true
```
*/
readonly deserializeComplexTypes?: boolean;
};

export type Migrations<T extends Record<string, unknown>> = Record<string, (store: Conf<T>) => void>;
Expand Down
Loading
Loading