From 8951e3f2c12f774f1683dfa451a1099f11a8532a Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 19 Dec 2025 14:20:25 +0100 Subject: [PATCH 1/8] fix: referential equalities DOS --- src/accessDeep.test.ts | 31 ------------------------ src/accessDeep.ts | 53 ------------------------------------------ src/index.test.ts | 12 ---------- src/index.ts | 8 +++---- src/plainer.ts | 14 ++++++++++- 5 files changed, 17 insertions(+), 101 deletions(-) delete mode 100644 src/accessDeep.test.ts diff --git a/src/accessDeep.test.ts b/src/accessDeep.test.ts deleted file mode 100644 index 5e64fad1..00000000 --- a/src/accessDeep.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { setDeep } from './accessDeep.js'; - -import { describe, it, expect } from 'vitest'; - -describe('setDeep', () => { - it('correctly sets values in maps', () => { - const obj = { - a: new Map([[new Set(['NaN']), [[1, 'undefined']]]]), - }; - - setDeep(obj, ['a', 0, 0, 0], Number); - setDeep(obj, ['a', 0, 1], entries => new Map(entries)); - setDeep(obj, ['a', 0, 1, 0, 1], () => undefined); - - expect(obj).toEqual({ - a: new Map([[new Set([NaN]), new Map([[1, undefined]])]]), - }); - }); - - it('correctly sets values in sets', () => { - const obj = { - a: new Set([10, new Set(['NaN'])]), - }; - - setDeep(obj, ['a', 1, 0], Number); - - expect(obj).toEqual({ - a: new Set([10, new Set([NaN])]), - }); - }); -}); diff --git a/src/accessDeep.ts b/src/accessDeep.ts index ea986130..2a56adc2 100644 --- a/src/accessDeep.ts +++ b/src/accessDeep.ts @@ -73,27 +73,6 @@ export const setDeep = ( parent = parent[index]; } else if (isPlainObject(parent)) { parent = parent[key]; - } else if (isSet(parent)) { - const row = +key; - parent = getNthKey(parent, row); - } else if (isMap(parent)) { - const isEnd = i === path.length - 2; - if (isEnd) { - break; - } - - const row = +key; - const type = +path[++i] === 0 ? 'key' : 'value'; - - const keyOfRow = getNthKey(parent, row); - switch (type) { - case 'key': - parent = keyOfRow; - break; - case 'value': - parent = parent.get(keyOfRow); - break; - } } } @@ -105,37 +84,5 @@ export const setDeep = ( parent[lastKey] = mapper(parent[lastKey]); } - if (isSet(parent)) { - const oldValue = getNthKey(parent, +lastKey); - const newValue = mapper(oldValue); - if (oldValue !== newValue) { - parent.delete(oldValue); - parent.add(newValue); - } - } - - if (isMap(parent)) { - const row = +path[path.length - 2]; - const keyToRow = getNthKey(parent, row); - - const type = +lastKey === 0 ? 'key' : 'value'; - switch (type) { - case 'key': { - const newKey = mapper(keyToRow); - parent.set(newKey, parent.get(keyToRow)); - - if (newKey !== keyToRow) { - parent.delete(keyToRow); - } - break; - } - - case 'value': { - parent.set(keyToRow, mapper(parent.get(keyToRow))); - break; - } - } - } - return object; }; diff --git a/src/index.test.ts b/src/index.test.ts index daea0194..b4e5046e 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1296,18 +1296,6 @@ test('dedupe=true on a large complicated schema', () => { expect(dedupedOut).toEqual(deserialized); }); -test('doesnt iterate to keys that dont exist', () => { - const robbyBubble = { id: 5 }; - const highscores = new Map([[robbyBubble, 5000]]); - const objectWithReferentialEquality = { highscores, topScorer: robbyBubble }; - const res = SuperJSON.serialize(objectWithReferentialEquality); - - expect(res.meta.referentialEqualities.topScorer).toEqual(['highscores.0.0']); - res.meta.referentialEqualities.topScorer = ['highscores.99999.0']; - - expect(() => SuperJSON.deserialize(res)).toThrowError('index out of bounds'); -}); - // https://github.com/flightcontrolhq/superjson/issues/319 test('deserialize in place', () => { const serialized = SuperJSON.serialize({ a: new Date() }); diff --git a/src/index.ts b/src/index.ts index 9a11ad74..2d898aa7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,10 +65,6 @@ export default class SuperJSON { let result: T = options?.inPlace ? json : copy(json) as any; - if (meta?.values) { - result = applyValueAnnotations(result, meta.values, meta.v ?? 0, this); - } - if (meta?.referentialEqualities) { result = applyReferentialEqualityAnnotations( result, @@ -77,6 +73,10 @@ export default class SuperJSON { ); } + if (meta?.values) { + result = applyValueAnnotations(result, meta.values, meta.v ?? 0, this); + } + return result; } diff --git a/src/plainer.ts b/src/plainer.ts index 8e9059f1..e97d968d 100644 --- a/src/plainer.ts +++ b/src/plainer.ts @@ -67,10 +67,22 @@ export function applyValueAnnotations( version: number, superJson: SuperJSON ) { + const transformedCache = new Map(); + traverse( annotations, (type, path) => { - plain = setDeep(plain, path, v => untransformValue(v, type, superJson)); + plain = setDeep(plain, path, v => { + const isPrimitive = v !== Object(v); + if (!isPrimitive && transformedCache.has(v)) { + return transformedCache.get(v); + } + const transformed = untransformValue(v, type, superJson); + if (!isPrimitive) { + transformedCache.set(v, transformed); + } + return transformed; + }); }, version ); From 37c8d213c8eeb206094fa74afb6ea7231d92faf4 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 19 Dec 2025 14:28:51 +0100 Subject: [PATCH 2/8] delete some more --- src/accessDeep.ts | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/src/accessDeep.ts b/src/accessDeep.ts index 2a56adc2..05c5966e 100644 --- a/src/accessDeep.ts +++ b/src/accessDeep.ts @@ -1,17 +1,6 @@ -import { isMap, isArray, isPlainObject, isSet } from './is.js'; +import { isArray, isPlainObject } from './is.js'; import { includes } from './util.js'; -const getNthKey = (value: Map | Set, n: number): any => { - if (n > value.size) throw new Error('index out of bounds'); - const keys = value.keys(); - while (n > 0) { - keys.next(); - n--; - } - - return keys.next().value; -}; - function validatePath(path: (string | number)[]) { if (includes(path, '__proto__')) { throw new Error('__proto__ is not allowed as a property'); @@ -29,24 +18,7 @@ export const getDeep = (object: object, path: (string | number)[]): object => { for (let i = 0; i < path.length; i++) { const key = path[i]; - if (isSet(object)) { - object = getNthKey(object, +key); - } else if (isMap(object)) { - const row = +key; - const type = +path[++i] === 0 ? 'key' : 'value'; - - const keyOfRow = getNthKey(object, row); - switch (type) { - case 'key': - object = keyOfRow; - break; - case 'value': - object = object.get(keyOfRow); - break; - } - } else { - object = (object as any)[key]; - } + object = (object as any)[key]; } return object; From fea27d5df0a9c13e5dc2f5db89dcc585f6de3ef7 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 19 Dec 2025 17:01:02 +0100 Subject: [PATCH 3/8] add regression tests --- src/index.test.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index b4e5046e..a676daf0 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -730,6 +730,54 @@ describe('stringify & parse', () => { }, }, }, + 'regression #347: shared regex': { + input: () => { + const regex = /shared-regex/g; + return { + a: regex, + b: regex, + }; + }, + output: { + a: '/shared-regex/g', + b: '/shared-regex/g', + }, + outputAnnotations: { + values: { + a: ['regexp'], + b: ['regexp'], + }, + referentialEqualities: { + a: ['b'], + }, + }, + customExpectations: output => { + expect(output.a).toBe(output.b); + }, + }, + 'regression #347: circular set': { + input: () => { + const set = new Set(); + set.add(set); + return { + a: set, + }; + }, + output: { + a: [null], + }, + outputAnnotations: { + values: { + a: ['set'], + }, + referentialEqualities: { + 'a': ['a.0'], + }, + }, + customExpectations: output => { + expect(output.a.values().next().value).toBe(output.a); + }, + } }; function deepFreeze(object: any, alreadySeenObjects = new Set()) { From d29c8ba32a2e72b6c995048c69c9d58788b56c39 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 19 Dec 2025 17:24:50 +0100 Subject: [PATCH 4/8] fix shared regex --- src/index.ts | 6 +++++- src/plainer.ts | 28 ++++++++++++++++------------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2d898aa7..eb0021f3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,16 +65,20 @@ export default class SuperJSON { let result: T = options?.inPlace ? json : copy(json) as any; + // TODO: figure out proper naming + const byproduct = new Map(); + if (meta?.referentialEqualities) { result = applyReferentialEqualityAnnotations( result, meta.referentialEqualities, + byproduct, meta.v ?? 0 ); } if (meta?.values) { - result = applyValueAnnotations(result, meta.values, meta.v ?? 0, this); + result = applyValueAnnotations(result, meta.values, byproduct, meta.v ?? 0, this); } return result; diff --git a/src/plainer.ts b/src/plainer.ts index e97d968d..a10b647a 100644 --- a/src/plainer.ts +++ b/src/plainer.ts @@ -64,24 +64,23 @@ function traverse( export function applyValueAnnotations( plain: any, annotations: MinimisedTree, + byproduct: Map, version: number, superJson: SuperJSON ) { - const transformedCache = new Map(); - traverse( annotations, (type, path) => { plain = setDeep(plain, path, v => { - const isPrimitive = v !== Object(v); - if (!isPrimitive && transformedCache.has(v)) { - return transformedCache.get(v); - } - const transformed = untransformValue(v, type, superJson); - if (!isPrimitive) { - transformedCache.set(v, transformed); + if (byproduct.has(stringifyPath(path))) { + const store = byproduct.get(stringifyPath(path))!; + if (store.empty) { + store.value = untransformValue(v, type, superJson); + store.empty = false; + } + return store.value; } - return transformed; + return untransformValue(v, type, superJson) }); }, version @@ -93,15 +92,20 @@ export function applyValueAnnotations( export function applyReferentialEqualityAnnotations( plain: any, annotations: ReferentialEqualityAnnotations, + byproduct: Map, version: number ) { const legacyPaths = enableLegacyPaths(version); function apply(identicalPaths: string[], path: string) { + const byproductStore = { value: null, empty: true }; + byproduct.set(path, byproductStore); + const object = getDeep(plain, parsePath(path, legacyPaths)); identicalPaths - .map(path => parsePath(path, legacyPaths)) - .forEach(identicalObjectPath => { + .forEach(path => { + byproduct.set(path, byproductStore); + const identicalObjectPath = parsePath(path, legacyPaths) plain = setDeep(plain, identicalObjectPath, () => object); }); } From 37cbb62ff7ab300102b324ad51c2ca86903e885a Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 19 Dec 2025 17:49:39 +0100 Subject: [PATCH 5/8] add note --- src/plainer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plainer.ts b/src/plainer.ts index a10b647a..bb904547 100644 --- a/src/plainer.ts +++ b/src/plainer.ts @@ -72,6 +72,7 @@ export function applyValueAnnotations( annotations, (type, path) => { plain = setDeep(plain, path, v => { + // TODO: also check legacy path if (byproduct.has(stringifyPath(path))) { const store = byproduct.get(stringifyPath(path))!; if (store.empty) { From 39aeb86f0a8285633ae8cbdb6ce04e66ed4c8445 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Sat, 20 Dec 2025 12:20:51 +0100 Subject: [PATCH 6/8] fix one more bug --- src/index.test.ts | 32 ++++++++++- src/index.ts | 17 +----- src/plainer.ts | 135 ++++++++++++++++++++++++++++------------------ 3 files changed, 114 insertions(+), 70 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index a676daf0..93fe9842 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -755,27 +755,55 @@ describe('stringify & parse', () => { expect(output.a).toBe(output.b); }, }, - 'regression #347: circular set': { + 'regression #347: circular set and map': { input: () => { const set = new Set(); set.add(set); + + const map = new Map(); + map.set(map, map); return { a: set, + b: map, }; }, output: { a: [null], + b: [[null, null]] }, outputAnnotations: { values: { a: ['set'], + b: ['map'], }, referentialEqualities: { 'a': ['a.0'], + 'b': ['b.0.0', 'b.0.1'] }, }, customExpectations: output => { expect(output.a.values().next().value).toBe(output.a); + expect(output.b.values().next().value).toBe(output.b); + }, + }, + 'regression #347: onyl referential equalities': { + input: () => { + const a = {}; + a['a'] = a; + return { + a: a, + }; + }, + output: { + a: { a: null }, + }, + outputAnnotations: { + referentialEqualities: { + 'a': ['a.a'], + }, + }, + customExpectations: output => { + expect(output.a).toBe(output.a.a); }, } }; @@ -894,7 +922,7 @@ describe('stringify & parse', () => { const { json, meta } = SuperJSON.serialize({ s7: new Train(100, 'yellow', 'Bombardier', new Set([new Carriage('front'), new Carriage('back')])) as any, }); - + expect(json).toEqual({ s7: { topSpeed: 100, diff --git a/src/index.ts b/src/index.ts index eb0021f3..ee16f7a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,6 @@ import { CustomTransformerRegistry, } from './custom-transformer-registry.js'; import { - applyReferentialEqualityAnnotations, applyValueAnnotations, generateReferentialEqualityAnnotations, walker, @@ -65,21 +64,7 @@ export default class SuperJSON { let result: T = options?.inPlace ? json : copy(json) as any; - // TODO: figure out proper naming - const byproduct = new Map(); - - if (meta?.referentialEqualities) { - result = applyReferentialEqualityAnnotations( - result, - meta.referentialEqualities, - byproduct, - meta.v ?? 0 - ); - } - - if (meta?.values) { - result = applyValueAnnotations(result, meta.values, byproduct, meta.v ?? 0, this); - } + result = applyValueAnnotations(result, meta?.values, meta?.referentialEqualities, meta?.v ?? 0, this); return result; } diff --git a/src/plainer.ts b/src/plainer.ts index bb904547..3978d053 100644 --- a/src/plainer.ts +++ b/src/plainer.ts @@ -64,25 +64,97 @@ function traverse( export function applyValueAnnotations( plain: any, annotations: MinimisedTree, - byproduct: Map, + referentialEqualityAnnotations: ReferentialEqualityAnnotations | undefined, version: number, superJson: SuperJSON ) { + const byproduct = new Map; + + if (referentialEqualityAnnotations !== undefined) { + const legacyPaths = enableLegacyPaths(version); + function apply(identicalPaths: string[], path: string) { + byproduct.set(path, identicalPaths); + } + + if (isArray(referentialEqualityAnnotations)) { + const [root, other] = referentialEqualityAnnotations; + root.forEach(identicalPath => { + plain = setDeep( + plain, + parsePath(identicalPath, legacyPaths), + () => plain + ); + }); + + if (other) { + forEach(other, apply); + } + } else { + forEach(referentialEqualityAnnotations, apply); + } + } + + const seen = byproduct.size ? new Set() : undefined; + + const pathsWithValueAnnotation = new Set(); + traverse( + annotations, + (type, path) => pathsWithValueAnnotation.add(stringifyPath(path)), + version + ) + + for (const [path, identicalPaths] of byproduct) { + if (pathsWithValueAnnotation.has(path)) + continue; + const original = getDeep(plain, parsePath(path, true)) as any; + for (const other of identicalPaths) + plain = setDeep(plain, parsePath(other, true), () => original) + } + traverse( annotations, (type, path) => { - plain = setDeep(plain, path, v => { - // TODO: also check legacy path - if (byproduct.has(stringifyPath(path))) { - const store = byproduct.get(stringifyPath(path))!; - if (store.empty) { - store.value = untransformValue(v, type, superJson); - store.empty = false; + if (seen?.has(stringifyPath(path))) + return; + + const identical = byproduct.get(stringifyPath(path)); + if (identical) { + identical.forEach(p => seen?.add(p)); + if (type === 'set') { + const oldValue = getDeep(plain, path) as any[]; + const newValue = new Set(); + for (const other of identical) { + plain = setDeep(plain, parsePath(other, false), () => newValue); + } + for (const value of oldValue) { + newValue.add(value); } - return store.value; + plain = setDeep(plain, path, () => newValue); + return; } - return untransformValue(v, type, superJson) - }); + + if (type === 'map') { + const oldValue = getDeep(plain, path) as [any, any][]; + const newValue = new Map(); + for (const other of identical) { + plain = setDeep(plain, parsePath(other, false), () => newValue); + } + for (const [key, value] of oldValue) { + newValue.set(key, value); + } + plain = setDeep(plain, path, () => newValue); + return; + } + + const oldValue = getDeep(plain, path); + const newValue = untransformValue(oldValue, type, superJson); + plain = setDeep(plain, path, () => newValue); + for (const other of identical) { + plain = setDeep(plain, parsePath(other, false), () => newValue) + } + } else { + plain = setDeep(plain, path, v => untransformValue(v, type, superJson)); + } }, version ); @@ -90,47 +162,6 @@ export function applyValueAnnotations( return plain; } -export function applyReferentialEqualityAnnotations( - plain: any, - annotations: ReferentialEqualityAnnotations, - byproduct: Map, - version: number -) { - const legacyPaths = enableLegacyPaths(version); - function apply(identicalPaths: string[], path: string) { - const byproductStore = { value: null, empty: true }; - byproduct.set(path, byproductStore); - - const object = getDeep(plain, parsePath(path, legacyPaths)); - - identicalPaths - .forEach(path => { - byproduct.set(path, byproductStore); - const identicalObjectPath = parsePath(path, legacyPaths) - plain = setDeep(plain, identicalObjectPath, () => object); - }); - } - - if (isArray(annotations)) { - const [root, other] = annotations; - root.forEach(identicalPath => { - plain = setDeep( - plain, - parsePath(identicalPath, legacyPaths), - () => plain - ); - }); - - if (other) { - forEach(other, apply); - } - } else { - forEach(annotations, apply); - } - - return plain; -} - const isDeep = (object: any, superJson: SuperJSON): boolean => isPlainObject(object) || isArray(object) || From 5a396cef7a53774515fe414220c40beb903c5e71 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Sat, 20 Dec 2025 12:40:18 +0100 Subject: [PATCH 7/8] some more tests --- src/index.test.ts | 31 ++++++++++++++++++++++++ src/plainer.ts | 60 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 93fe9842..272f677e 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -786,6 +786,37 @@ describe('stringify & parse', () => { expect(output.b.values().next().value).toBe(output.b); }, }, + 'regression #347: circular set in root': { + input: () => { + const set = new Set(); + set.add(set); + return set; + }, + output: [null], + outputAnnotations: { + values: ['set'], + referentialEqualities: [['0']], + }, + customExpectations: output => { + expect(output.values().next().value).toBe(output); + }, + }, + 'regression #347: circular map in root': { + input: () => { + const map = new Map(); + map.set(map, map); + return map; + }, + output: [[null, null]], + outputAnnotations: { + values: ['map'], + referentialEqualities: [['0.0', '0.1']], + }, + customExpectations: output => { + expect(output.values().next().value).toBe(output); + expect(output.keys().next().value).toBe(output); + }, + }, 'regression #347: onyl referential equalities': { input: () => { const a = {}; diff --git a/src/plainer.ts b/src/plainer.ts index 3978d053..47f3ba28 100644 --- a/src/plainer.ts +++ b/src/plainer.ts @@ -69,23 +69,16 @@ export function applyValueAnnotations( superJson: SuperJSON ) { const byproduct = new Map; + let rootIdentities: string[] | undefined; if (referentialEqualityAnnotations !== undefined) { - const legacyPaths = enableLegacyPaths(version); function apply(identicalPaths: string[], path: string) { byproduct.set(path, identicalPaths); } if (isArray(referentialEqualityAnnotations)) { const [root, other] = referentialEqualityAnnotations; - root.forEach(identicalPath => { - plain = setDeep( - plain, - parsePath(identicalPath, legacyPaths), - () => plain - ); - }); - + rootIdentities = root; if (other) { forEach(other, apply); } @@ -97,9 +90,15 @@ export function applyValueAnnotations( const seen = byproduct.size ? new Set() : undefined; const pathsWithValueAnnotation = new Set(); + let rootValueAnnotation: TypeAnnotation | undefined = undefined; traverse( annotations, - (type, path) => pathsWithValueAnnotation.add(stringifyPath(path)), + (type, path) => { + if (path.length === 0) + rootValueAnnotation = type; + else + pathsWithValueAnnotation.add(stringifyPath(path)) + }, version ) @@ -110,10 +109,51 @@ export function applyValueAnnotations( for (const other of identicalPaths) plain = setDeep(plain, parsePath(other, true), () => original) } + if (false && rootIdentities && !rootValueAnnotation) { + for (const other of rootIdentities) + plain = setDeep(plain, parsePath(other, true), () => plain) + } traverse( annotations, (type, path) => { + if (path.length === 0) { + if (rootIdentities) { + if (type === 'set') { + const newValue = new Set(); + for (const other of rootIdentities) { + plain = setDeep(plain, parsePath(other, false), () => newValue); + } + for (const value of plain) { + newValue.add(value) + } + plain = newValue; + return; + } + if (type === 'map') { + const newValue = new Map(); + for (const other of rootIdentities) { + plain = setDeep(plain, parsePath(other, false), () => newValue); + } + for (const [key, value] of plain) { + newValue.set(key, value); + } + plain = newValue; + return; + } + + const oldValue = getDeep(plain, path); + const newValue = untransformValue(oldValue, type, superJson); + plain = setDeep(plain, path, () => newValue); + for (const other of rootIdentities) { + plain = setDeep(plain, parsePath(other, false), () => newValue) + } + } else { + plain = untransformValue(plain, type, superJson) + } + return; + } + if (seen?.has(stringifyPath(path))) return; From 7c2746211e0681263cbad043bd1b7516a228cd39 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Sat, 20 Dec 2025 12:43:29 +0100 Subject: [PATCH 8/8] some more --- src/plainer.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/plainer.ts b/src/plainer.ts index 47f3ba28..98d53846 100644 --- a/src/plainer.ts +++ b/src/plainer.ts @@ -109,7 +109,7 @@ export function applyValueAnnotations( for (const other of identicalPaths) plain = setDeep(plain, parsePath(other, true), () => original) } - if (false && rootIdentities && !rootValueAnnotation) { + if (rootIdentities && !rootValueAnnotation) { for (const other of rootIdentities) plain = setDeep(plain, parsePath(other, true), () => plain) } @@ -142,12 +142,7 @@ export function applyValueAnnotations( return; } - const oldValue = getDeep(plain, path); - const newValue = untransformValue(oldValue, type, superJson); - plain = setDeep(plain, path, () => newValue); - for (const other of rootIdentities) { - plain = setDeep(plain, parsePath(other, false), () => newValue) - } + throw new Error("If my understanding of the code is correct, this is unreachable") } else { plain = untransformValue(plain, type, superJson) }