hardened API decoding against server fields the models did not expect

The Talk room list crashed for a new account because the server omits
lastMessage for rooms without a visible message and the model required it.
Beyond that single field, a review of the Talk models against the OpenAPI
spec of the installed version (23.0.8) found the same crash class in several
places: closed enums over open server values (inCall bitmask, rich-object
types, actor and message types), a deleted parent sent as {id, deleted},
and the autocomplete status arriving as an object.

- json_serializable `checked: true`: decode errors now name class and field
- lists (rooms, messages, participants, autocomplete) decode per entry;
  a malformed entry is skipped and reported via ClientErrorReporter
- unknownEnumValue fallbacks for all response enums of Talk and breakers
- lastMessage nullable; inCall as bitmask; deleted parent placeholder;
  autocomplete status object/string; fixed shareWithDisplayNameUnique key
- GetRoomParams no longer sends null query parameters

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrepDTL4qDe35TuS29sLW3
This commit is contained in:
2026-09-29 16:35:41 +02:00
co-authored by Claude Fable 5.1
parent a79b376bb3
commit 279b973e89
92 changed files with 2150 additions and 963 deletions
+17 -8
View File
@@ -8,14 +8,23 @@ part of 'chat_background_settings.dart';
ChatBackgroundSettings _$ChatBackgroundSettingsFromJson(
Map<String, dynamic> json,
) => ChatBackgroundSettings(
type: $enumDecode(_$ChatBackgroundTypeEnumMap, json['type']),
fit: $enumDecode(_$ChatBackgroundFitEnumMap, json['fit']),
colorValue: (json['colorValue'] as num?)?.toInt(),
imageVersion: (json['imageVersion'] as num).toInt(),
dim: (json['dim'] as num).toDouble(),
blur: (json['blur'] as num).toDouble(),
);
) => $checkedCreate('ChatBackgroundSettings', json, ($checkedConvert) {
final val = ChatBackgroundSettings(
type: $checkedConvert(
'type',
(v) => $enumDecode(_$ChatBackgroundTypeEnumMap, v),
),
fit: $checkedConvert(
'fit',
(v) => $enumDecode(_$ChatBackgroundFitEnumMap, v),
),
colorValue: $checkedConvert('colorValue', (v) => (v as num?)?.toInt()),
imageVersion: $checkedConvert('imageVersion', (v) => (v as num).toInt()),
dim: $checkedConvert('dim', (v) => (v as num).toDouble()),
blur: $checkedConvert('blur', (v) => (v as num).toDouble()),
);
return val;
});
Map<String, dynamic> _$ChatBackgroundSettingsToJson(
ChatBackgroundSettings instance,
+43 -17
View File
@@ -7,23 +7,49 @@ part of 'dev_tools_settings.dart';
// **************************************************************************
DevToolsSettings _$DevToolsSettingsFromJson(Map<String, dynamic> json) =>
DevToolsSettings(
showPerformanceOverlay: json['showPerformanceOverlay'] as bool,
showFrameStats: json['showFrameStats'] as bool? ?? false,
showMemoryStats: json['showMemoryStats'] as bool? ?? false,
showSemanticsDebugger: json['showSemanticsDebugger'] as bool? ?? false,
slowAnimations: json['slowAnimations'] as bool? ?? false,
textScaleOverride: (json['textScaleOverride'] as num?)?.toDouble() ?? 0,
marianumConnectEndpoint:
$enumDecodeNullable(
_$MarianumConnectEndpointEnumMap,
json['marianumConnectEndpoint'],
) ??
MarianumConnectEndpoint.live,
marianumConnectCustomUrl:
json['marianumConnectCustomUrl'] as String? ?? '',
emergencyNoticeUrl: json['emergencyNoticeUrl'] as String? ?? '',
);
$checkedCreate('DevToolsSettings', json, ($checkedConvert) {
final val = DevToolsSettings(
showPerformanceOverlay: $checkedConvert(
'showPerformanceOverlay',
(v) => v as bool,
),
showFrameStats: $checkedConvert(
'showFrameStats',
(v) => v as bool? ?? false,
),
showMemoryStats: $checkedConvert(
'showMemoryStats',
(v) => v as bool? ?? false,
),
showSemanticsDebugger: $checkedConvert(
'showSemanticsDebugger',
(v) => v as bool? ?? false,
),
slowAnimations: $checkedConvert(
'slowAnimations',
(v) => v as bool? ?? false,
),
textScaleOverride: $checkedConvert(
'textScaleOverride',
(v) => (v as num?)?.toDouble() ?? 0,
),
marianumConnectEndpoint: $checkedConvert(
'marianumConnectEndpoint',
(v) =>
$enumDecodeNullable(_$MarianumConnectEndpointEnumMap, v) ??
MarianumConnectEndpoint.live,
),
marianumConnectCustomUrl: $checkedConvert(
'marianumConnectCustomUrl',
(v) => v as String? ?? '',
),
emergencyNoticeUrl: $checkedConvert(
'emergencyNoticeUrl',
(v) => v as String? ?? '',
),
);
return val;
});
Map<String, dynamic> _$DevToolsSettingsToJson(DevToolsSettings instance) =>
<String, dynamic>{
+12 -5
View File
@@ -6,11 +6,18 @@ part of 'file_settings.dart';
// JsonSerializableGenerator
// **************************************************************************
FileSettings _$FileSettingsFromJson(Map<String, dynamic> json) => FileSettings(
sortFoldersToTop: json['sortFoldersToTop'] as bool,
ascending: json['ascending'] as bool,
sortBy: $enumDecode(_$SortOptionEnumMap, json['sortBy']),
);
FileSettings _$FileSettingsFromJson(Map<String, dynamic> json) =>
$checkedCreate('FileSettings', json, ($checkedConvert) {
final val = FileSettings(
sortFoldersToTop: $checkedConvert('sortFoldersToTop', (v) => v as bool),
ascending: $checkedConvert('ascending', (v) => v as bool),
sortBy: $checkedConvert(
'sortBy',
(v) => $enumDecode(_$SortOptionEnumMap, v),
),
);
return val;
});
Map<String, dynamic> _$FileSettingsToJson(FileSettings instance) =>
<String, dynamic>{
+9 -3
View File
@@ -7,9 +7,15 @@ part of 'file_view_settings.dart';
// **************************************************************************
FileViewSettings _$FileViewSettingsFromJson(Map<String, dynamic> json) =>
FileViewSettings(
alwaysOpenExternally: json['alwaysOpenExternally'] as bool,
);
$checkedCreate('FileViewSettings', json, ($checkedConvert) {
final val = FileViewSettings(
alwaysOpenExternally: $checkedConvert(
'alwaysOpenExternally',
(v) => v as bool,
),
);
return val;
});
Map<String, dynamic> _$FileViewSettingsToJson(FileViewSettings instance) =>
<String, dynamic>{'alwaysOpenExternally': instance.alwaysOpenExternally};
+9 -1
View File
@@ -7,7 +7,15 @@ part of 'haptic_settings.dart';
// **************************************************************************
HapticSettings _$HapticSettingsFromJson(Map<String, dynamic> json) =>
HapticSettings(level: $enumDecode(_$HapticLevelEnumMap, json['level']));
$checkedCreate('HapticSettings', json, ($checkedConvert) {
final val = HapticSettings(
level: $checkedConvert(
'level',
(v) => $enumDecode(_$HapticLevelEnumMap, v),
),
);
return val;
});
Map<String, dynamic> _$HapticSettingsToJson(HapticSettings instance) =>
<String, dynamic>{'level': _$HapticLevelEnumMap[instance.level]!};
+10 -4
View File
@@ -7,10 +7,16 @@ part of 'holidays_settings.dart';
// **************************************************************************
HolidaysSettings _$HolidaysSettingsFromJson(Map<String, dynamic> json) =>
HolidaysSettings(
dismissedDisclaimer: json['dismissedDisclaimer'] as bool,
showPastEvents: json['showPastEvents'] as bool,
);
$checkedCreate('HolidaysSettings', json, ($checkedConvert) {
final val = HolidaysSettings(
dismissedDisclaimer: $checkedConvert(
'dismissedDisclaimer',
(v) => v as bool,
),
showPastEvents: $checkedConvert('showPastEvents', (v) => v as bool),
);
return val;
});
Map<String, dynamic> _$HolidaysSettingsToJson(HolidaysSettings instance) =>
<String, dynamic>{
+29 -11
View File
@@ -7,17 +7,35 @@ part of 'modules_settings.dart';
// **************************************************************************
ModulesSettings _$ModulesSettingsFromJson(Map<String, dynamic> json) =>
ModulesSettings(
moduleOrder: (json['moduleOrder'] as List<dynamic>)
.map((e) => $enumDecode(_$ModulesEnumMap, e))
.toList(),
hiddenModules: (json['hiddenModules'] as List<dynamic>)
.map((e) => $enumDecode(_$ModulesEnumMap, e))
.toList(),
autoFillBottomBar: json['autoFillBottomBar'] as bool? ?? true,
fixedBottomBarSlots: (json['fixedBottomBarSlots'] as num?)?.toInt() ?? 3,
teacherDefaultsApplied: json['teacherDefaultsApplied'] as bool? ?? false,
);
$checkedCreate('ModulesSettings', json, ($checkedConvert) {
final val = ModulesSettings(
moduleOrder: $checkedConvert(
'moduleOrder',
(v) => (v as List<dynamic>)
.map((e) => $enumDecode(_$ModulesEnumMap, e))
.toList(),
),
hiddenModules: $checkedConvert(
'hiddenModules',
(v) => (v as List<dynamic>)
.map((e) => $enumDecode(_$ModulesEnumMap, e))
.toList(),
),
autoFillBottomBar: $checkedConvert(
'autoFillBottomBar',
(v) => v as bool? ?? true,
),
fixedBottomBarSlots: $checkedConvert(
'fixedBottomBarSlots',
(v) => (v as num?)?.toInt() ?? 3,
),
teacherDefaultsApplied: $checkedConvert(
'teacherDefaultsApplied',
(v) => v as bool? ?? false,
),
);
return val;
});
Map<String, dynamic> _$ModulesSettingsToJson(
ModulesSettings instance,
+10 -5
View File
@@ -8,11 +8,16 @@ part of 'notification_settings.dart';
NotificationSettings _$NotificationSettingsFromJson(
Map<String, dynamic> json,
) => NotificationSettings(
enabled: json['enabled'] as bool? ?? true,
talkPermissionPromptShown:
json['talkPermissionPromptShown'] as bool? ?? false,
);
) => $checkedCreate('NotificationSettings', json, ($checkedConvert) {
final val = NotificationSettings(
enabled: $checkedConvert('enabled', (v) => v as bool? ?? true),
talkPermissionPromptShown: $checkedConvert(
'talkPermissionPromptShown',
(v) => v as bool? ?? false,
),
);
return val;
});
Map<String, dynamic> _$NotificationSettingsToJson(
NotificationSettings instance,
+55 -37
View File
@@ -6,43 +6,61 @@ part of 'settings.dart';
// JsonSerializableGenerator
// **************************************************************************
Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
appTheme: Settings._themeFromJson(json['appTheme'] as String),
devToolsEnabled: json['devToolsEnabled'] as bool,
modulesSettings: ModulesSettings.fromJson(
json['modulesSettings'] as Map<String, dynamic>,
),
timetableSettings: TimetableSettings.fromJson(
json['timetableSettings'] as Map<String, dynamic>,
),
timetableFavoritesSettings: TimetableFavoritesSettings.fromJson(
json['timetableFavoritesSettings'] as Map<String, dynamic>,
),
talkSettings: TalkSettings.fromJson(
json['talkSettings'] as Map<String, dynamic>,
),
chatBackgroundSettings: ChatBackgroundSettings.fromJson(
json['chatBackgroundSettings'] as Map<String, dynamic>,
),
fileSettings: FileSettings.fromJson(
json['fileSettings'] as Map<String, dynamic>,
),
holidaysSettings: HolidaysSettings.fromJson(
json['holidaysSettings'] as Map<String, dynamic>,
),
fileViewSettings: FileViewSettings.fromJson(
json['fileViewSettings'] as Map<String, dynamic>,
),
notificationSettings: NotificationSettings.fromJson(
json['notificationSettings'] as Map<String, dynamic>,
),
devToolsSettings: DevToolsSettings.fromJson(
json['devToolsSettings'] as Map<String, dynamic>,
),
hapticSettings: HapticSettings.fromJson(
json['hapticSettings'] as Map<String, dynamic>,
),
);
Settings _$SettingsFromJson(Map<String, dynamic> json) =>
$checkedCreate('Settings', json, ($checkedConvert) {
final val = Settings(
appTheme: $checkedConvert(
'appTheme',
(v) => Settings._themeFromJson(v as String),
),
devToolsEnabled: $checkedConvert('devToolsEnabled', (v) => v as bool),
modulesSettings: $checkedConvert(
'modulesSettings',
(v) => ModulesSettings.fromJson(v as Map<String, dynamic>),
),
timetableSettings: $checkedConvert(
'timetableSettings',
(v) => TimetableSettings.fromJson(v as Map<String, dynamic>),
),
timetableFavoritesSettings: $checkedConvert(
'timetableFavoritesSettings',
(v) => TimetableFavoritesSettings.fromJson(v as Map<String, dynamic>),
),
talkSettings: $checkedConvert(
'talkSettings',
(v) => TalkSettings.fromJson(v as Map<String, dynamic>),
),
chatBackgroundSettings: $checkedConvert(
'chatBackgroundSettings',
(v) => ChatBackgroundSettings.fromJson(v as Map<String, dynamic>),
),
fileSettings: $checkedConvert(
'fileSettings',
(v) => FileSettings.fromJson(v as Map<String, dynamic>),
),
holidaysSettings: $checkedConvert(
'holidaysSettings',
(v) => HolidaysSettings.fromJson(v as Map<String, dynamic>),
),
fileViewSettings: $checkedConvert(
'fileViewSettings',
(v) => FileViewSettings.fromJson(v as Map<String, dynamic>),
),
notificationSettings: $checkedConvert(
'notificationSettings',
(v) => NotificationSettings.fromJson(v as Map<String, dynamic>),
),
devToolsSettings: $checkedConvert(
'devToolsSettings',
(v) => DevToolsSettings.fromJson(v as Map<String, dynamic>),
),
hapticSettings: $checkedConvert(
'hapticSettings',
(v) => HapticSettings.fromJson(v as Map<String, dynamic>),
),
);
return val;
});
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'appTheme': Settings._themeToJson(instance.appTheme),
+19 -6
View File
@@ -6,12 +6,25 @@ part of 'talk_settings.dart';
// JsonSerializableGenerator
// **************************************************************************
TalkSettings _$TalkSettingsFromJson(Map<String, dynamic> json) => TalkSettings(
sortFavoritesToTop: json['sortFavoritesToTop'] as bool,
sortUnreadToTop: json['sortUnreadToTop'] as bool,
drafts: Map<String, String>.from(json['drafts'] as Map),
draftReplies: Map<String, int>.from(json['draftReplies'] as Map),
);
TalkSettings _$TalkSettingsFromJson(Map<String, dynamic> json) =>
$checkedCreate('TalkSettings', json, ($checkedConvert) {
final val = TalkSettings(
sortFavoritesToTop: $checkedConvert(
'sortFavoritesToTop',
(v) => v as bool,
),
sortUnreadToTop: $checkedConvert('sortUnreadToTop', (v) => v as bool),
drafts: $checkedConvert(
'drafts',
(v) => Map<String, String>.from(v as Map),
),
draftReplies: $checkedConvert(
'draftReplies',
(v) => Map<String, int>.from(v as Map),
),
);
return val;
});
Map<String, dynamic> _$TalkSettingsToJson(TalkSettings instance) =>
<String, dynamic>{
+24 -10
View File
@@ -8,11 +8,17 @@ part of 'timetable_favorites_settings.dart';
FavoriteTimetableElement _$FavoriteTimetableElementFromJson(
Map<String, dynamic> json,
) => FavoriteTimetableElement(
type: $enumDecode(_$TimetableElementTypeEnumMap, json['type']),
id: (json['id'] as num).toInt(),
label: json['label'] as String,
);
) => $checkedCreate('FavoriteTimetableElement', json, ($checkedConvert) {
final val = FavoriteTimetableElement(
type: $checkedConvert(
'type',
(v) => $enumDecode(_$TimetableElementTypeEnumMap, v),
),
id: $checkedConvert('id', (v) => (v as num).toInt()),
label: $checkedConvert('label', (v) => v as String),
);
return val;
});
Map<String, dynamic> _$FavoriteTimetableElementToJson(
FavoriteTimetableElement instance,
@@ -31,11 +37,19 @@ const _$TimetableElementTypeEnumMap = {
TimetableFavoritesSettings _$TimetableFavoritesSettingsFromJson(
Map<String, dynamic> json,
) => TimetableFavoritesSettings(
favorites: (json['favorites'] as List<dynamic>)
.map((e) => FavoriteTimetableElement.fromJson(e as Map<String, dynamic>))
.toList(),
);
) => $checkedCreate('TimetableFavoritesSettings', json, ($checkedConvert) {
final val = TimetableFavoritesSettings(
favorites: $checkedConvert(
'favorites',
(v) => (v as List<dynamic>)
.map(
(e) => FavoriteTimetableElement.fromJson(e as Map<String, dynamic>),
)
.toList(),
),
);
return val;
});
Map<String, dynamic> _$TimetableFavoritesSettingsToJson(
TimetableFavoritesSettings instance,
+13 -7
View File
@@ -7,13 +7,19 @@ part of 'timetable_settings.dart';
// **************************************************************************
TimetableSettings _$TimetableSettingsFromJson(Map<String, dynamic> json) =>
TimetableSettings(
connectDoubleLessons: json['connectDoubleLessons'] as bool,
timetableNameMode: $enumDecode(
_$TimetableNameModeEnumMap,
json['timetableNameMode'],
),
);
$checkedCreate('TimetableSettings', json, ($checkedConvert) {
final val = TimetableSettings(
connectDoubleLessons: $checkedConvert(
'connectDoubleLessons',
(v) => v as bool,
),
timetableNameMode: $checkedConvert(
'timetableNameMode',
(v) => $enumDecode(_$TimetableNameModeEnumMap, v),
),
);
return val;
});
Map<String, dynamic> _$TimetableSettingsToJson(
TimetableSettings instance,