fast-json-stringify 5.12.0 → 5.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/benchmark/bench.js +24 -0
- package/index.js +17 -1
- package/lib/serializer.js +16 -28
- package/package.json +3 -3
- package/test/basic.test.js +6 -0
- package/test/string.test.js +34 -0
- package/types/index.d.ts +5 -2
- package/types/index.test-d.ts +79 -57
package/README.md
CHANGED
|
@@ -630,6 +630,24 @@ integer-like values, such as:
|
|
|
630
630
|
- `'2e4'` - _note this will be converted to `2`, not `20000`_
|
|
631
631
|
- `1.5` - _note this will be converted to `1`_
|
|
632
632
|
|
|
633
|
+
<a name="unsafe"></a>
|
|
634
|
+
#### Unsafe string
|
|
635
|
+
By default, the library escapes all strings. With the 'unsafe' format, the string isn't escaped. This has a potentially dangerous security issue. You can use it only if you are sure that your data doesn't need escaping. The advantage is a significant performance improvement.
|
|
636
|
+
|
|
637
|
+
Example:
|
|
638
|
+
```javascript
|
|
639
|
+
const stringify = fastJson({
|
|
640
|
+
title: 'Example Schema',
|
|
641
|
+
type: 'object',
|
|
642
|
+
properties: {
|
|
643
|
+
'code': {
|
|
644
|
+
type: 'string',
|
|
645
|
+
format 'unsafe'
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
})
|
|
649
|
+
```
|
|
650
|
+
|
|
633
651
|
##### Benchmarks
|
|
634
652
|
|
|
635
653
|
For reference, here goes some benchmarks for comparison over the three
|
package/benchmark/bench.js
CHANGED
|
@@ -47,6 +47,14 @@ const benchmarks = [
|
|
|
47
47
|
},
|
|
48
48
|
input: 'hello world'
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
name: 'unsafe short string',
|
|
52
|
+
schema: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
format: 'unsafe'
|
|
55
|
+
},
|
|
56
|
+
input: 'hello world'
|
|
57
|
+
},
|
|
50
58
|
{
|
|
51
59
|
name: 'short string with double quote',
|
|
52
60
|
schema: {
|
|
@@ -61,6 +69,14 @@ const benchmarks = [
|
|
|
61
69
|
},
|
|
62
70
|
input: longSimpleString
|
|
63
71
|
},
|
|
72
|
+
{
|
|
73
|
+
name: 'unsafe long string without double quotes',
|
|
74
|
+
schema: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
format: 'unsafe'
|
|
77
|
+
},
|
|
78
|
+
input: longSimpleString
|
|
79
|
+
},
|
|
64
80
|
{
|
|
65
81
|
name: 'long string',
|
|
66
82
|
schema: {
|
|
@@ -68,6 +84,14 @@ const benchmarks = [
|
|
|
68
84
|
},
|
|
69
85
|
input: longString
|
|
70
86
|
},
|
|
87
|
+
{
|
|
88
|
+
name: 'unsafe long string',
|
|
89
|
+
schema: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
format: 'unsafe'
|
|
92
|
+
},
|
|
93
|
+
input: longString
|
|
94
|
+
},
|
|
71
95
|
{
|
|
72
96
|
name: 'number',
|
|
73
97
|
schema: {
|
package/index.js
CHANGED
|
@@ -725,8 +725,24 @@ function buildSingleTypeSerializer (context, location, input) {
|
|
|
725
725
|
return `json += serializer.asDate(${input})`
|
|
726
726
|
} else if (schema.format === 'time') {
|
|
727
727
|
return `json += serializer.asTime(${input})`
|
|
728
|
+
} else if (schema.format === 'unsafe') {
|
|
729
|
+
return `json += serializer.asUnsafeString(${input})`
|
|
728
730
|
} else {
|
|
729
|
-
return `
|
|
731
|
+
return `
|
|
732
|
+
if (typeof ${input} !== 'string') {
|
|
733
|
+
if (${input} === null) {
|
|
734
|
+
json += '""'
|
|
735
|
+
} else if (${input} instanceof Date) {
|
|
736
|
+
json += '"' + ${input}.toISOString() + '"'
|
|
737
|
+
} else if (${input} instanceof RegExp) {
|
|
738
|
+
json += serializer.asString(${input}.source)
|
|
739
|
+
} else {
|
|
740
|
+
json += serializer.asString(${input}.toString())
|
|
741
|
+
}
|
|
742
|
+
} else {
|
|
743
|
+
json += serializer.asString(${input})
|
|
744
|
+
}
|
|
745
|
+
`
|
|
730
746
|
}
|
|
731
747
|
}
|
|
732
748
|
case 'integer':
|
package/lib/serializer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
// eslint-disable-next-line
|
|
4
|
-
const STR_ESCAPE = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]
|
|
4
|
+
const STR_ESCAPE = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/
|
|
5
5
|
|
|
6
6
|
module.exports = class Serializer {
|
|
7
7
|
constructor (options) {
|
|
@@ -25,13 +25,12 @@ module.exports = class Serializer {
|
|
|
25
25
|
|
|
26
26
|
asInteger (i) {
|
|
27
27
|
if (typeof i === 'number') {
|
|
28
|
-
if (i === Infinity || i === -Infinity) {
|
|
29
|
-
throw new Error(`The value "${i}" cannot be converted to an integer.`)
|
|
30
|
-
}
|
|
31
28
|
if (Number.isInteger(i)) {
|
|
32
29
|
return '' + i
|
|
33
30
|
}
|
|
34
|
-
if
|
|
31
|
+
// check if number is Infinity or NaN
|
|
32
|
+
// eslint-disable-next-line no-self-compare
|
|
33
|
+
if (i === Infinity || i === -Infinity || i !== i) {
|
|
35
34
|
throw new Error(`The value "${i}" cannot be converted to an integer.`)
|
|
36
35
|
}
|
|
37
36
|
return this.parseInteger(i)
|
|
@@ -52,7 +51,9 @@ module.exports = class Serializer {
|
|
|
52
51
|
|
|
53
52
|
asNumber (i) {
|
|
54
53
|
const num = Number(i)
|
|
55
|
-
if
|
|
54
|
+
// check if number is NaN
|
|
55
|
+
// eslint-disable-next-line no-self-compare
|
|
56
|
+
if (num !== num) {
|
|
56
57
|
throw new Error(`The value "${i}" cannot be converted to a number.`)
|
|
57
58
|
} else if (!Number.isFinite(num)) {
|
|
58
59
|
return 'null'
|
|
@@ -99,29 +100,20 @@ module.exports = class Serializer {
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
asString (str) {
|
|
102
|
-
if (typeof str !== 'string') {
|
|
103
|
-
if (str === null) {
|
|
104
|
-
return '""'
|
|
105
|
-
}
|
|
106
|
-
if (str instanceof Date) {
|
|
107
|
-
return '"' + str.toISOString() + '"'
|
|
108
|
-
}
|
|
109
|
-
if (str instanceof RegExp) {
|
|
110
|
-
str = str.source
|
|
111
|
-
} else {
|
|
112
|
-
str = str.toString()
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
103
|
if (str.length < 42) {
|
|
117
104
|
return this.asStringSmall(str)
|
|
118
|
-
} else if (STR_ESCAPE.test(str) === false) {
|
|
105
|
+
} else if (str.length < 5000 && STR_ESCAPE.test(str) === false) {
|
|
106
|
+
// Only use the regular expression for shorter input. The overhead is otherwise too much.
|
|
119
107
|
return '"' + str + '"'
|
|
120
108
|
} else {
|
|
121
109
|
return JSON.stringify(str)
|
|
122
110
|
}
|
|
123
111
|
}
|
|
124
112
|
|
|
113
|
+
asUnsafeString (str) {
|
|
114
|
+
return '"' + str + '"'
|
|
115
|
+
}
|
|
116
|
+
|
|
125
117
|
// magically escape strings for json
|
|
126
118
|
// relying on their charCodeAt
|
|
127
119
|
// everything below 32 needs JSON.stringify()
|
|
@@ -137,13 +129,6 @@ module.exports = class Serializer {
|
|
|
137
129
|
// eslint-disable-next-line
|
|
138
130
|
for (var i = 0; i < len; i++) {
|
|
139
131
|
point = str.charCodeAt(i)
|
|
140
|
-
if (point < 32) {
|
|
141
|
-
return JSON.stringify(str)
|
|
142
|
-
}
|
|
143
|
-
if (point >= 0xD800 && point <= 0xDFFF) {
|
|
144
|
-
// The current character is a surrogate.
|
|
145
|
-
return JSON.stringify(str)
|
|
146
|
-
}
|
|
147
132
|
if (
|
|
148
133
|
point === 0x22 || // '"'
|
|
149
134
|
point === 0x5c // '\'
|
|
@@ -151,6 +136,9 @@ module.exports = class Serializer {
|
|
|
151
136
|
last === -1 && (last = 0)
|
|
152
137
|
result += str.slice(last, i) + '\\'
|
|
153
138
|
last = i
|
|
139
|
+
} else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) {
|
|
140
|
+
// The current character is non-printable characters or a surrogate.
|
|
141
|
+
return JSON.stringify(str)
|
|
154
142
|
}
|
|
155
143
|
}
|
|
156
144
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fast-json-stringify",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.14.0",
|
|
4
4
|
"description": "Stringify your JSON at max speed",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -46,13 +46,13 @@
|
|
|
46
46
|
"simple-git": "^3.7.1",
|
|
47
47
|
"standard": "^17.0.0",
|
|
48
48
|
"tap": "^16.0.1",
|
|
49
|
-
"tsd": "^0.
|
|
49
|
+
"tsd": "^0.31.0",
|
|
50
50
|
"webpack": "^5.40.0",
|
|
51
51
|
"fast-json-stringify": "."
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"ajv": "^8.10.0",
|
|
55
|
-
"ajv-formats": "^
|
|
55
|
+
"ajv-formats": "^3.0.1",
|
|
56
56
|
"fast-deep-equal": "^3.1.3",
|
|
57
57
|
"fast-uri": "^2.1.0",
|
|
58
58
|
"rfdc": "^1.2.0",
|
package/test/basic.test.js
CHANGED
package/test/string.test.js
CHANGED
|
@@ -48,3 +48,37 @@ test('serialize long string', (t) => {
|
|
|
48
48
|
t.equal(output, `"${new Array(2e4).fill('\\u0000').join('')}"`)
|
|
49
49
|
t.equal(JSON.parse(output), input)
|
|
50
50
|
})
|
|
51
|
+
|
|
52
|
+
test('unsafe string', (t) => {
|
|
53
|
+
t.plan(2)
|
|
54
|
+
|
|
55
|
+
const schema = {
|
|
56
|
+
type: 'string',
|
|
57
|
+
format: 'unsafe'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const input = 'abcd'
|
|
61
|
+
const stringify = build(schema)
|
|
62
|
+
const output = stringify(input)
|
|
63
|
+
|
|
64
|
+
t.equal(output, `"${input}"`)
|
|
65
|
+
t.equal(JSON.parse(output), input)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('unsafe unescaped string', (t) => {
|
|
69
|
+
t.plan(2)
|
|
70
|
+
|
|
71
|
+
const schema = {
|
|
72
|
+
type: 'string',
|
|
73
|
+
format: 'unsafe'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const input = 'abcd "abcd"'
|
|
77
|
+
const stringify = build(schema)
|
|
78
|
+
const output = stringify(input)
|
|
79
|
+
|
|
80
|
+
t.equal(output, `"${input}"`)
|
|
81
|
+
t.throws(function () {
|
|
82
|
+
JSON.parse(output)
|
|
83
|
+
})
|
|
84
|
+
})
|
package/types/index.d.ts
CHANGED
|
@@ -208,6 +208,9 @@ interface StandaloneOption extends build.Options {
|
|
|
208
208
|
mode: 'standalone'
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
type StringCoercible = string | Date | RegExp;
|
|
212
|
+
type IntegerCoercible = number | BigInt;
|
|
213
|
+
|
|
211
214
|
/**
|
|
212
215
|
* Build a stringify function using a schema of the documents that should be stringified
|
|
213
216
|
* @param schema The schema used to stringify values
|
|
@@ -217,8 +220,8 @@ declare function build(schema: build.AnySchema, options: DebugOption): { code: s
|
|
|
217
220
|
declare function build(schema: build.AnySchema, options: DeprecateDebugOption): { code: string, ajv: Ajv };
|
|
218
221
|
declare function build(schema: build.AnySchema, options: StandaloneOption): string;
|
|
219
222
|
declare function build(schema: build.AnySchema, options?: build.Options): <TDoc = any>(doc: TDoc) => any;
|
|
220
|
-
declare function build(schema: build.StringSchema, options?: build.Options): <TDoc extends
|
|
221
|
-
declare function build(schema: build.IntegerSchema | build.NumberSchema, options?: build.Options): <TDoc extends
|
|
223
|
+
declare function build(schema: build.StringSchema, options?: build.Options): <TDoc extends StringCoercible = StringCoercible>(doc: TDoc) => string;
|
|
224
|
+
declare function build(schema: build.IntegerSchema | build.NumberSchema, options?: build.Options): <TDoc extends IntegerCoercible = IntegerCoercible>(doc: TDoc) => string;
|
|
222
225
|
declare function build(schema: build.NullSchema, options?: build.Options): <TDoc extends null = null>(doc: TDoc) => "null";
|
|
223
226
|
declare function build(schema: build.BooleanSchema, options?: build.Options): <TDoc extends boolean = boolean>(doc: TDoc) => string;
|
|
224
227
|
declare function build(schema: build.ArraySchema | build.TupleSchema, options?: build.Options): <TDoc extends any[]= any[]>(doc: TDoc) => string;
|
package/types/index.test-d.ts
CHANGED
|
@@ -3,61 +3,62 @@ import build, { restore, Schema, validLargeArrayMechanisms } from '..'
|
|
|
3
3
|
import { expectError, expectType } from 'tsd'
|
|
4
4
|
|
|
5
5
|
// Number schemas
|
|
6
|
-
|
|
6
|
+
build({
|
|
7
7
|
type: 'number'
|
|
8
|
-
}
|
|
9
|
-
|
|
8
|
+
})(25)
|
|
9
|
+
build({
|
|
10
10
|
type: 'integer'
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
})(-5)
|
|
12
|
+
build({
|
|
13
|
+
type: 'integer'
|
|
14
|
+
})(5n)
|
|
15
15
|
|
|
16
|
-
build(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
build(
|
|
20
|
-
|
|
16
|
+
build({
|
|
17
|
+
type: 'number'
|
|
18
|
+
}, { rounding: 'ceil' })
|
|
19
|
+
build({
|
|
20
|
+
type: 'number'
|
|
21
|
+
}, { rounding: 'floor' })
|
|
22
|
+
build({
|
|
23
|
+
type: 'number'
|
|
24
|
+
}, { rounding: 'round' })
|
|
25
|
+
build({
|
|
26
|
+
type: 'number'
|
|
27
|
+
}, { rounding: 'trunc' })
|
|
28
|
+
expectError(build({
|
|
29
|
+
type: 'number'
|
|
30
|
+
}, { rounding: 'invalid' }))
|
|
21
31
|
|
|
22
32
|
// String schema
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
build(schema3)('foobar')
|
|
33
|
+
build({
|
|
34
|
+
type: 'string'
|
|
35
|
+
})('foobar')
|
|
28
36
|
|
|
29
37
|
// Boolean schema
|
|
30
|
-
|
|
38
|
+
build({
|
|
31
39
|
type: 'boolean'
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
build(schema4)(true)
|
|
40
|
+
})(true)
|
|
35
41
|
|
|
36
42
|
// Null schema
|
|
37
|
-
|
|
43
|
+
build({
|
|
38
44
|
type: 'null'
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
build(schema5)(null)
|
|
45
|
+
})(null)
|
|
42
46
|
|
|
43
47
|
// Array schemas
|
|
44
|
-
|
|
48
|
+
build({
|
|
45
49
|
type: 'array',
|
|
46
50
|
items: { type: 'number' }
|
|
47
|
-
}
|
|
48
|
-
|
|
51
|
+
})([25])
|
|
52
|
+
build({
|
|
49
53
|
type: 'array',
|
|
50
54
|
items: [{ type: 'string'}, {type: 'integer'}]
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
build(schema6)([25])
|
|
54
|
-
build(schema7)(['hello', 42])
|
|
55
|
+
})(['hello', 42])
|
|
55
56
|
|
|
56
57
|
// Object schemas
|
|
57
|
-
|
|
58
|
+
build({
|
|
58
59
|
type: 'object'
|
|
59
|
-
}
|
|
60
|
-
|
|
60
|
+
})({})
|
|
61
|
+
build({
|
|
61
62
|
type: 'object',
|
|
62
63
|
properties: {
|
|
63
64
|
foo: { type: 'string' },
|
|
@@ -70,14 +71,24 @@ const schema9: Schema = {
|
|
|
70
71
|
additionalProperties: {
|
|
71
72
|
type: 'boolean'
|
|
72
73
|
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
})({ foo: 'bar' })
|
|
75
|
+
build({
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
foo: { type: 'string' },
|
|
79
|
+
bar: { type: 'integer' }
|
|
80
|
+
},
|
|
81
|
+
required: ['foo'],
|
|
82
|
+
patternProperties: {
|
|
83
|
+
'baz*': { type: 'null' }
|
|
84
|
+
},
|
|
85
|
+
additionalProperties: {
|
|
86
|
+
type: 'boolean'
|
|
87
|
+
}
|
|
88
|
+
}, { rounding: 'floor' })({ foo: 'bar' })
|
|
78
89
|
|
|
79
90
|
// Reference schemas
|
|
80
|
-
|
|
91
|
+
build({
|
|
81
92
|
title: 'Example Schema',
|
|
82
93
|
definitions: {
|
|
83
94
|
num: {
|
|
@@ -109,12 +120,10 @@ const schema10: Schema = {
|
|
|
109
120
|
additionalProperties: {
|
|
110
121
|
$ref: '#/definitions/def'
|
|
111
122
|
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
build(schema10)({ nickname: '', num: { int: 5 }, other: null })
|
|
123
|
+
})({ nickname: '', num: { int: 5 }, other: null })
|
|
115
124
|
|
|
116
125
|
// Conditional/Combined schemas
|
|
117
|
-
|
|
126
|
+
build({
|
|
118
127
|
title: 'Conditional/Combined Schema',
|
|
119
128
|
type: 'object',
|
|
120
129
|
properties: {
|
|
@@ -140,24 +149,37 @@ const schema11: Schema = {
|
|
|
140
149
|
somethingElse: { type: 'null' }
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
build(schema11)({ something: 'a string', somethingElse: 42 })
|
|
152
|
+
})({ something: 'a string', somethingElse: 42 })
|
|
146
153
|
|
|
147
154
|
// String schema with format
|
|
148
|
-
|
|
155
|
+
|
|
156
|
+
build({
|
|
149
157
|
type: 'string',
|
|
150
158
|
format: 'date-time'
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
build(schema12)(new Date())
|
|
159
|
+
})(new Date())
|
|
154
160
|
|
|
161
|
+
/*
|
|
162
|
+
This overload doesn't work yet -
|
|
163
|
+
TypeScript chooses the generic for the schema
|
|
164
|
+
before it chooses the overload for the options
|
|
165
|
+
parameter.
|
|
155
166
|
let str: string, ajv: Ajv
|
|
156
|
-
str = build(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
ajv = build(
|
|
160
|
-
|
|
167
|
+
str = build({
|
|
168
|
+
type: 'number'
|
|
169
|
+
}, { debugMode: true }).code
|
|
170
|
+
ajv = build({
|
|
171
|
+
type: 'number'
|
|
172
|
+
}, { debugMode: true }).ajv
|
|
173
|
+
str = build({
|
|
174
|
+
type: 'number'
|
|
175
|
+
}, { mode: 'debug' }).code
|
|
176
|
+
ajv = build({
|
|
177
|
+
type: 'number'
|
|
178
|
+
}, { mode: 'debug' }).ajv
|
|
179
|
+
str = build({
|
|
180
|
+
type: 'number'
|
|
181
|
+
}, { mode: 'standalone' })
|
|
182
|
+
*/
|
|
161
183
|
|
|
162
184
|
const debugCompiled = build({
|
|
163
185
|
title: 'default string',
|