Skip to content

Commit 1de45ee

Browse files
committed
Always write web view discriminators to JSON
WebViewProperties.fromJson picks the subclass from `webviewType`, and the Google Maps and Twitter readers pick theirs from `mapMode` and `embedType`. Each subclass sets these with a field initializer, which the defaults builder took for a default, so toJson left them out and no version could read a web view node back ("type 'Null' is not a subtype of type 'String'"). The builder no longer treats the initializer of a field that overrides a supertype's field as a default. Only the web view discriminators change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YaHo5D7kD3eWcagvzh3o3K
1 parent 1b6901a commit 1de45ee

5 files changed

Lines changed: 74 additions & 45 deletions

File tree

‎lib/src/api/nodes/web_view_google_maps_properties.g.dart‎

Lines changed: 0 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎lib/src/api/nodes/web_view_node.g.dart‎

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎lib/src/api/nodes/web_view_twitter_properties.g.dart‎

Lines changed: 0 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/web_view_json_test.dart‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import 'dart:convert';
2+
3+
import 'package:codelessly_api/codelessly_api.dart';
4+
import 'package:test/test.dart';
5+
6+
Map<String, dynamic> webView(String id, Map<String, dynamic> properties) => {
7+
'id': id,
8+
'name': 'Web View',
9+
'type': 'webView',
10+
'basicBoxLocal': [0, 0, 300, 200],
11+
'properties': properties,
12+
};
13+
14+
void main() {
15+
// WebViewProperties.fromJson picks the subclass from webviewType, and the
16+
// Google Maps and Twitter readers pick theirs from mapMode and embedType,
17+
// so these keys must be written even though each subclass has only one
18+
// value for them.
19+
final samples = {
20+
'webpage': webView('page', {
21+
'webviewType': 'webpage',
22+
'pageSourceType': 'url',
23+
'input': 'https://example.com',
24+
}),
25+
'googleMaps': webView('maps', {
26+
'webviewType': 'googleMaps',
27+
'mapMode': 'places',
28+
'query': 'Coffee',
29+
'url': 'https://maps.example.com',
30+
}),
31+
'twitter': webView('tweet', {
32+
'webviewType': 'twitter',
33+
'embedType': 'tweet',
34+
'tweetURL': 'https://x.com/a/status/1',
35+
'mediaAutoPlaybackPolicy': 'requireUserActionForAllMedia',
36+
}),
37+
};
38+
39+
for (final MapEntry(key: kind, value: input) in samples.entries) {
40+
test('$kind web view writes its discriminators and reads back', () {
41+
final node = NodeJsonConverter().fromJson(input)!;
42+
final json = jsonDecode(jsonEncode(node.toJson())) as Map;
43+
final properties = json['properties'] as Map;
44+
expect(properties['webviewType'], kind);
45+
if (kind == 'googleMaps') expect(properties['mapMode'], 'places');
46+
if (kind == 'twitter') expect(properties['embedType'], 'tweet');
47+
48+
final back = NodeJsonConverter().fromJson(json)!;
49+
expect(jsonDecode(jsonEncode(back.toJson())), json);
50+
});
51+
}
52+
}

‎tool/json_defaults_builder/lib/builder.dart‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
/// 4. The field's own initializer, if the field is set through that
1414
/// constructor or is mutable, and the initializer is a constant-like
1515
/// expression. A final field without a constructor parameter (such as a
16-
/// node's `type`) is always written.
16+
/// node's `type`) is always written, and so is a field that overrides a
17+
/// supertype's field (such as `webviewType` in a `WebViewProperties`
18+
/// subclass), because polymorphic readers pick the subclass from it.
1719
///
1820
/// Fields annotated with `@JsonOmitIf(test)` are also removed when
1921
/// `test(instance)` is true.
@@ -97,7 +99,8 @@ class JsonDefaultsGenerator extends GeneratorForAnnotation<JsonSerializable> {
9799
_annotationArgument(declaration, 'JsonLegacyDefault') ??
98100
_jsonKeyDefaultValue(declaration) ??
99101
paramDefaults[name] ??
100-
(paramNames.contains(name) || !field.isFinal
102+
((paramNames.contains(name) || !field.isFinal) &&
103+
!_overridesSuperField(field)
101104
? _simpleInitializer(declaration)
102105
: null);
103106
if (defaultCode != null) {
@@ -121,6 +124,23 @@ ${checks.join('\n')}
121124
''';
122125
}
123126

127+
/// Whether [field] overrides a field of a supertype of the class that
128+
/// declares it, like `late WebViewType webviewType = WebViewType.webpage`
129+
/// in a subclass of `WebViewProperties`.
130+
///
131+
/// Such an initializer tells the subclasses apart rather than being a
132+
/// default: polymorphic readers such as `WebViewProperties.fromJson` pick
133+
/// the subclass from the key, so it must always be written.
134+
bool _overridesSuperField(FieldElement field) {
135+
final owner = field.enclosingElement;
136+
if (owner is! InterfaceElement) return false;
137+
return owner.allSupertypes.any(
138+
(type) => type.element.fields.any(
139+
(other) => !other.isStatic && other.name == field.name,
140+
),
141+
);
142+
}
143+
124144
/// Public instance fields of [element] and its superclasses, closest first.
125145
Iterable<FieldElement> _serializableFields(ClassElement element) sync* {
126146
final classes = <InterfaceElement>[

0 commit comments

Comments
 (0)