Skip to content

Commit 15a2386

Browse files
perf: optimize required props serialization (#675)
1 parent d95ef87 commit 15a2386

File tree

3 files changed

+55
-68
lines changed

3 files changed

+55
-68
lines changed

index.js

Lines changed: 50 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ const validLargeArrayMechanisms = [
2727
'json-stringify'
2828
]
2929

30-
const addComma = '!addComma && (addComma = true) || (json += \',\')'
31-
3230
let schemaIdCounter = 0
3331

3432
const mergedSchemaRef = Symbol('fjs-merged-schema-ref')
@@ -262,7 +260,7 @@ function inferTypeByKeyword (schema) {
262260
return schema.type
263261
}
264262

265-
function buildExtraObjectPropertiesSerializer (context, location) {
263+
function buildExtraObjectPropertiesSerializer (context, location, addComma) {
266264
const schema = location.schema
267265
const propertiesKeys = Object.keys(schema.properties || {})
268266

@@ -321,88 +319,76 @@ function buildExtraObjectPropertiesSerializer (context, location) {
321319
}
322320

323321
function buildInnerObject (context, location) {
324-
let code = ''
325322
const schema = location.schema
326-
const required = schema.required || []
327323

328324
const propertiesLocation = location.getPropertyLocation('properties')
325+
const requiredProperties = schema.required || []
326+
327+
// Should serialize required properties first
328+
const propertiesKeys = Object.keys(schema.properties || {}).sort(
329+
(key1, key2) => {
330+
const required1 = requiredProperties.includes(key1)
331+
const required2 = requiredProperties.includes(key2)
332+
return required1 === required2 ? 0 : required1 ? -1 : 1
333+
}
334+
)
335+
const hasRequiredProperties = requiredProperties.includes(propertiesKeys[0])
329336

330-
const requiredWithDefault = []
331-
const requiredWithoutDefault = []
332-
if (schema.properties) {
333-
for (const key of Object.keys(schema.properties)) {
334-
if (required.indexOf(key) === -1) {
335-
continue
336-
}
337-
let propertyLocation = propertiesLocation.getPropertyLocation(key)
338-
if (propertyLocation.schema.$ref) {
339-
propertyLocation = resolveRef(context, propertyLocation)
340-
}
341-
342-
const sanitizedKey = JSON.stringify(key)
337+
let code = ''
343338

344-
// Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
345-
// see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
346-
const defaultValue = propertyLocation.schema.default
347-
if (defaultValue === undefined) {
348-
code += `if (obj[${sanitizedKey}] === undefined) throw new Error('${sanitizedKey} is required!')\n`
349-
requiredWithoutDefault.push(key)
350-
}
351-
requiredWithDefault.push(key)
339+
for (const key of requiredProperties) {
340+
if (!propertiesKeys.includes(key)) {
341+
code += `if (obj['${key}'] === undefined) throw new Error('"${key}" is required!')\n`
352342
}
353343
}
354344

355-
// handle extraneous required fields
356-
for (const requiredProperty of required) {
357-
if (requiredWithDefault.indexOf(requiredProperty) !== -1) continue
358-
code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n`
359-
}
345+
code += 'let json = \'{\'\n'
360346

361-
code += `
362-
let addComma = false
363-
let json = '{'
364-
`
347+
let addComma = ''
348+
if (!hasRequiredProperties) {
349+
code += 'let addComma = false\n'
350+
addComma = '!addComma && (addComma = true) || (json += \',\')'
351+
}
365352

366-
if (schema.properties) {
367-
for (const key of Object.keys(schema.properties)) {
368-
let propertyLocation = propertiesLocation.getPropertyLocation(key)
369-
if (propertyLocation.schema.$ref) {
370-
propertyLocation = resolveRef(context, propertyLocation)
371-
}
353+
for (const key of propertiesKeys) {
354+
let propertyLocation = propertiesLocation.getPropertyLocation(key)
355+
if (propertyLocation.schema.$ref) {
356+
propertyLocation = resolveRef(context, propertyLocation)
357+
}
372358

373-
const sanitizedKey = JSON.stringify(key)
359+
const sanitizedKey = JSON.stringify(key)
360+
const defaultValue = propertyLocation.schema.default
361+
const isRequired = requiredProperties.includes(key)
374362

375-
if (requiredWithoutDefault.indexOf(key) !== -1) {
376-
code += `
363+
code += `
364+
if (obj[${sanitizedKey}] !== undefined) {
377365
${addComma}
378366
json += ${JSON.stringify(sanitizedKey + ':')}
379367
${buildValue(context, propertyLocation, `obj[${sanitizedKey}]`)}
368+
}`
369+
370+
if (defaultValue !== undefined) {
371+
code += ` else {
372+
${addComma}
373+
json += ${JSON.stringify(sanitizedKey + ':' + JSON.stringify(defaultValue))}
374+
}
380375
`
381-
} else {
382-
// Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
383-
// see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
384-
code += `
385-
if (obj[${sanitizedKey}] !== undefined) {
386-
${addComma}
387-
json += ${JSON.stringify(sanitizedKey + ':')}
388-
${buildValue(context, propertyLocation, `obj[${sanitizedKey}]`)}
389-
}
390-
`
391-
const defaultValue = propertyLocation.schema.default
392-
if (defaultValue !== undefined) {
393-
code += `
394-
else {
395-
${addComma}
396-
json += ${JSON.stringify(sanitizedKey + ':' + JSON.stringify(defaultValue))}
397-
}
398-
`
399-
}
376+
} else if (isRequired) {
377+
code += ` else {
378+
throw new Error('${sanitizedKey} is required!')
400379
}
380+
`
381+
} else {
382+
code += '\n'
383+
}
384+
385+
if (hasRequiredProperties) {
386+
addComma = 'json += \',\''
401387
}
402388
}
403389

404390
if (schema.patternProperties || schema.additionalProperties) {
405-
code += buildExtraObjectPropertiesSerializer(context, location)
391+
code += buildExtraObjectPropertiesSerializer(context, location, addComma)
406392
}
407393

408394
code += `

test/allof.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ test('object with allOf and multiple schema on the allOf', (t) => {
121121
id: 1,
122122
name: 'string',
123123
tag: 'otherString'
124-
}), '{"name":"string","tag":"otherString","id":1}')
124+
}), '{"name":"string","id":1,"tag":"otherString"}')
125125
})
126126

127127
test('object with allOf and one schema on the allOf', (t) => {

test/required.test.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,17 +126,18 @@ test('object with multiple required field not in properties schema', (t) => {
126126
stringify({})
127127
t.fail()
128128
} catch (e) {
129-
t.equal(e.message, '"num" is required!')
129+
t.equal(e.message, '"key1" is required!')
130130
t.pass()
131131
}
132132

133133
try {
134134
stringify({
135-
num: 42
135+
key1: 42,
136+
key2: 42
136137
})
137138
t.fail()
138139
} catch (e) {
139-
t.equal(e.message, '"key1" is required!')
140+
t.equal(e.message, '"num" is required!')
140141
t.pass()
141142
}
142143

0 commit comments

Comments
 (0)