Skip to content

Commit 6ade578

Browse files
committed
fix(runtime): stop eager json parsing for unknown and any type bindings
1 parent d7ee800 commit 6ade578

File tree

4 files changed

+91
-20
lines changed

4 files changed

+91
-20
lines changed

src/runtime/parse-property-value.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,6 @@ export const parsePropertyValue = (propValue: unknown, propType: number, isFormA
3939
return propValue;
4040
}
4141

42-
/**
43-
* For custom types (Unknown) and Any types, attempt JSON parsing if the value looks like JSON.
44-
* This provides consistent behavior between SSR and non-SSR for complex types.
45-
* We do this before the primitive type checks to ensure custom types get object parsing.
46-
*/
47-
if (
48-
typeof propValue === 'string' &&
49-
(propType & MEMBER_FLAGS.Unknown || propType & MEMBER_FLAGS.Any) &&
50-
((propValue.startsWith('{') && propValue.endsWith('}')) || (propValue.startsWith('[') && propValue.endsWith(']')))
51-
) {
52-
try {
53-
return JSON.parse(propValue);
54-
} catch (e) {
55-
// If JSON parsing fails, continue with normal processing
56-
}
57-
}
58-
5942
if (propValue != null && !isComplexType(propValue)) {
6043
/**
6144
* ensure this value is of the correct prop type

src/runtime/test/parse-property-value.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ describe('parse-property-value', () => {
246246
});
247247

248248
describe('JSON parsing for custom types', () => {
249-
describe('MEMBER_FLAGS.Unknown (custom interfaces)', () => {
249+
describe.skip('MEMBER_FLAGS.Unknown (custom interfaces)', () => {
250250
it('parses JSON object strings for Unknown types', () => {
251251
const jsonString = '{"param":"Foo Bar","count":42}';
252252
const result = parsePropertyValue(jsonString, MEMBER_FLAGS.Unknown);
@@ -278,7 +278,7 @@ describe('parse-property-value', () => {
278278
});
279279
});
280280

281-
describe('MEMBER_FLAGS.Any', () => {
281+
describe.skip('MEMBER_FLAGS.Any', () => {
282282
it('parses JSON object strings for Any types', () => {
283283
const jsonString = '{"param":"Foo Bar","count":42}';
284284
const result = parsePropertyValue(jsonString, MEMBER_FLAGS.Any);

src/runtime/test/prop.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ describe('prop', () => {
209209
`);
210210
});
211211

212-
it('should demonstrate JSON parsing for complex object props', async () => {
212+
it.skip('should demonstrate JSON parsing for complex object props', async () => {
213213
@Component({ tag: 'simple-demo' })
214214
class SimpleDemo {
215215
@Prop() message: { text: string } = { text: 'default' };
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Component, h, Prop } from '@stencil/core';
2+
import { newSpecPage } from '@stencil/core/testing';
3+
4+
/**
5+
* Regression tests for:
6+
* - #6368: input/textarea values containing JSON should not be coerced to objects during change/assignment
7+
* - #6380: prop typed as a union (e.g. string | number) must not parse a valid JSON string into an object
8+
*/
9+
10+
describe('regression: do not parse JSON strings into objects', () => {
11+
it('does not parse JSON when assigning to a union prop (string | number)', async () => {
12+
@Component({ tag: 'cmp-union' })
13+
class CmpUnion {
14+
@Prop() value!: string | number;
15+
render() {
16+
return <div>{typeof this.value}:{String(this.value)}</div>;
17+
}
18+
}
19+
20+
const json = '{"text":"Hello"}';
21+
22+
const page = await newSpecPage({
23+
components: [CmpUnion],
24+
html: `<cmp-union value='${json}'></cmp-union>`,
25+
});
26+
27+
// Expect the prop to remain a string and not be parsed to an object
28+
expect(page.root?.textContent).toBe(`string:${json}`);
29+
});
30+
31+
it('does not parse JSON when assigning to a union prop (string | boolean)', async () => {
32+
@Component({ tag: 'cmp-union-bool' })
33+
class CmpUnionBool {
34+
@Prop() value!: string | boolean;
35+
render() {
36+
return <div>{typeof this.value}:{String(this.value)}</div>;
37+
}
38+
}
39+
40+
const json = '{"active":true}';
41+
42+
const page = await newSpecPage({
43+
components: [CmpUnionBool],
44+
html: `<cmp-union-bool value='${json}'></cmp-union-bool>`,
45+
});
46+
47+
expect(page.root?.textContent).toBe(`string:${json}`);
48+
});
49+
50+
it('does not parse JSON from an <input> value propagated to a mutable string prop', async () => {
51+
@Component({ tag: 'cmp-input-bind' })
52+
class CmpInputBind {
53+
// emulates how frameworks pass raw input values to components
54+
@Prop({ mutable: true, reflect: true }) value: string = '';
55+
56+
private onInput = (ev: Event) => {
57+
const target = ev.target as HTMLInputElement;
58+
this.value = target.value; // assigning raw value must not parse JSON
59+
};
60+
61+
render() {
62+
return (
63+
<div>
64+
<input value={this.value} onInput={this.onInput} />
65+
<span id="out">{typeof this.value}:{this.value}</span>
66+
</div>
67+
);
68+
}
69+
}
70+
71+
const page = await newSpecPage({
72+
components: [CmpInputBind],
73+
html: `<cmp-input-bind></cmp-input-bind>`,
74+
});
75+
76+
const input = page.root!.querySelector('input')! as HTMLInputElement;
77+
const json = '{"a":1}';
78+
79+
// simulate user typing JSON into the input
80+
input.value = json;
81+
// Use a standard 'input' Event to mirror how other hydration tests trigger input handlers
82+
input.dispatchEvent(new Event('input', { bubbles: true }));
83+
await page.waitForChanges();
84+
85+
const out = page.root!.querySelector('#out')! as HTMLSpanElement;
86+
expect(out.textContent).toBe(`string:${json}`);
87+
});
88+
});

0 commit comments

Comments
 (0)