diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart
new file mode 100644
index 0000000000..7b27f59aee
--- /dev/null
+++ b/mobile/lib/utils/openapi_patching.dart
@@ -0,0 +1,12 @@
+import 'package:openapi/api.dart';
+
+dynamic upgradeDto(dynamic value, String targetType) {
+  switch (targetType) {
+    case 'UserPreferencesResponseDto':
+      if (value is Map) {
+        if (value['rating'] == null) {
+          value['rating'] = RatingResponse().toJson();
+        }
+      }
+  }
+}
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 19ff7fc6d5..bbe680731e 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -16,6 +16,7 @@ import 'dart:io';
 
 import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
+import 'package:immich_mobile/utils/openapi_patching.dart';
 import 'package:http/http.dart';
 import 'package:intl/intl.dart';
 import 'package:meta/meta.dart';
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index 346eee3f50..01c646d393 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -166,6 +166,7 @@ class ApiClient {
 
   /// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
   static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
+    upgradeDto(value, targetType);
     try {
       switch (targetType) {
         case 'String':
diff --git a/mobile/openapi/lib/model/rating_response.dart b/mobile/openapi/lib/model/rating_response.dart
index 80ef5980fb..31505550ef 100644
--- a/mobile/openapi/lib/model/rating_response.dart
+++ b/mobile/openapi/lib/model/rating_response.dart
@@ -13,7 +13,7 @@ part of openapi.api;
 class RatingResponse {
   /// Returns a new [RatingResponse] instance.
   RatingResponse({
-    required this.enabled,
+    this.enabled = false,
   });
 
   bool enabled;
diff --git a/mobile/openapi/pubspec.yaml b/mobile/openapi/pubspec.yaml
index f033028432..4a979bf5db 100644
--- a/mobile/openapi/pubspec.yaml
+++ b/mobile/openapi/pubspec.yaml
@@ -13,5 +13,5 @@ dependencies:
   http: '>=0.13.0 <0.14.0'
   intl: any
   meta: '^1.1.8'
-dev_dependencies:
-  test: '>=1.21.6 <1.22.0'
+  immich_mobile:
+    path: ../
diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh
index a00d57d0ae..bf79b0bd82 100755
--- a/open-api/bin/generate-open-api.sh
+++ b/open-api/bin/generate-open-api.sh
@@ -8,12 +8,18 @@ function dart {
   cd ./templates/mobile/serialization/native
   wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache
   patch --no-backup-if-mismatch -u native_class.mustache <native_class.mustache.patch
-  cd ../../../..
+
+  cd ../../
+  wget -O api_client.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/api_client.mustache
+  patch --no-backup-if-mismatch -u api_client.mustache <api_client.mustache.patch
+
+  cd ../../
   npx --yes @openapitools/openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./templates/mobile
 
   # Post generate patches
   patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./patch/api_client.dart.patch
   patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api.dart <./patch/api.dart.patch
+  patch --no-backup-if-mismatch -u ../mobile/openapi/pubspec.yaml <./patch/pubspec_immich_mobile.yaml.patch
   # Don't include analysis_options.yaml for the generated openapi files
   # so that language servers can properly exclude the mobile/openapi directory
   rm ../mobile/openapi/analysis_options.yaml
@@ -34,4 +40,4 @@ elif [[ $1 == 'typescript' ]]; then
 else
   dart
   typescript
-fi
+fi
\ No newline at end of file
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index 78aaf78e94..91e32d1e05 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -9918,6 +9918,7 @@
       "RatingResponse": {
         "properties": {
           "enabled": {
+            "default": false,
             "type": "boolean"
           }
         },
diff --git a/open-api/patch/api.dart.patch b/open-api/patch/api.dart.patch
index 3bb8dadb41..12e80c92af 100644
--- a/open-api/patch/api.dart.patch
+++ b/open-api/patch/api.dart.patch
@@ -1,8 +1,9 @@
-@@ -15,6 +15,7 @@
+@@ -15,6 +15,8 @@
  import 'dart:io';
 
  import 'package:collection/collection.dart';
 +import 'package:flutter/foundation.dart';
++import 'package:immich_mobile/utils/openapi_patching.dart';
  import 'package:http/http.dart';
  import 'package:intl/intl.dart';
  import 'package:meta/meta.dart';
diff --git a/open-api/patch/pubspec_immich_mobile.yaml.patch b/open-api/patch/pubspec_immich_mobile.yaml.patch
new file mode 100644
index 0000000000..5f15c7254a
--- /dev/null
+++ b/open-api/patch/pubspec_immich_mobile.yaml.patch
@@ -0,0 +1,9 @@
+# Include code from immich_mobile
+@@ -13,5 +13,5 @@
+   http: '>=0.13.0 <0.14.0'
+   intl: any
+   meta: '^1.1.8'
+-dev_dependencies:
+-  test: '>=1.21.6 <1.22.0'
++  immich_mobile:
++    path: ../
diff --git a/open-api/templates/mobile/api_client.mustache b/open-api/templates/mobile/api_client.mustache
new file mode 100644
index 0000000000..7f464f026e
--- /dev/null
+++ b/open-api/templates/mobile/api_client.mustache
@@ -0,0 +1,264 @@
+{{>header}}
+{{>part_of}}
+class ApiClient {
+  ApiClient({this.basePath = '{{{basePath}}}', this.authentication,});
+
+  final String basePath;
+  final Authentication? authentication;
+
+  var _client = Client();
+  final _defaultHeaderMap = <String, String>{};
+
+  /// Returns the current HTTP [Client] instance to use in this class.
+  ///
+  /// The return value is guaranteed to never be null.
+  Client get client => _client;
+
+  /// Requests to use a new HTTP [Client] in this class.
+  set client(Client newClient) {
+    _client = newClient;
+  }
+
+  Map<String, String> get defaultHeaderMap => _defaultHeaderMap;
+
+  void addDefaultHeader(String key, String value) {
+     _defaultHeaderMap[key] = value;
+  }
+
+  // We don't use a Map<String, String> for queryParams.
+  // If collectionFormat is 'multi', a key might appear multiple times.
+  Future<Response> invokeAPI(
+    String path,
+    String method,
+    List<QueryParam> queryParams,
+    Object? body,
+    Map<String, String> headerParams,
+    Map<String, String> formParams,
+    String? contentType,
+  ) async {
+    await authentication?.applyToParams(queryParams, headerParams);
+
+    headerParams.addAll(_defaultHeaderMap);
+    if (contentType != null) {
+      headerParams['Content-Type'] = contentType;
+    }
+
+    final urlEncodedQueryParams = queryParams.map((param) => '$param');
+    final queryString = urlEncodedQueryParams.isNotEmpty ? '?${urlEncodedQueryParams.join('&')}' : '';
+    final uri = Uri.parse('$basePath$path$queryString');
+
+    try {
+      // Special case for uploading a single file which isn't a 'multipart/form-data'.
+      if (
+        body is MultipartFile && (contentType == null ||
+        !contentType.toLowerCase().startsWith('multipart/form-data'))
+      ) {
+        final request = StreamedRequest(method, uri);
+        request.headers.addAll(headerParams);
+        request.contentLength = body.length;
+        body.finalize().listen(
+          request.sink.add,
+          onDone: request.sink.close,
+          // ignore: avoid_types_on_closure_parameters
+          onError: (Object error, StackTrace trace) => request.sink.close(),
+          cancelOnError: true,
+        );
+        final response = await _client.send(request);
+        return Response.fromStream(response);
+      }
+
+      if (body is MultipartRequest) {
+        final request = MultipartRequest(method, uri);
+        request.fields.addAll(body.fields);
+        request.files.addAll(body.files);
+        request.headers.addAll(body.headers);
+        request.headers.addAll(headerParams);
+        final response = await _client.send(request);
+        return Response.fromStream(response);
+      }
+
+      final msgBody = contentType == 'application/x-www-form-urlencoded'
+        ? formParams
+        : await serializeAsync(body);
+      final nullableHeaderParams = headerParams.isEmpty ? null : headerParams;
+
+      switch(method) {
+        case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,);
+        case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,);
+        case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,);
+        case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,);
+        case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,);
+        case 'GET': return await _client.get(uri, headers: nullableHeaderParams,);
+      }
+    } on SocketException catch (error, trace) {
+      throw ApiException.withInner(
+        HttpStatus.badRequest,
+        'Socket operation failed: $method $path',
+        error,
+        trace,
+      );
+    } on TlsException catch (error, trace) {
+      throw ApiException.withInner(
+        HttpStatus.badRequest,
+        'TLS/SSL communication failed: $method $path',
+        error,
+        trace,
+      );
+    } on IOException catch (error, trace) {
+      throw ApiException.withInner(
+        HttpStatus.badRequest,
+        'I/O operation failed: $method $path',
+        error,
+        trace,
+      );
+    } on ClientException catch (error, trace) {
+      throw ApiException.withInner(
+        HttpStatus.badRequest,
+        'HTTP connection failed: $method $path',
+        error,
+        trace,
+      );
+    } on Exception catch (error, trace) {
+      throw ApiException.withInner(
+        HttpStatus.badRequest,
+        'Exception occurred: $method $path',
+        error,
+        trace,
+      );
+    }
+
+    throw ApiException(
+      HttpStatus.badRequest,
+      'Invalid HTTP operation: $method $path',
+    );
+  }
+{{#native_serialization}}
+
+  Future<dynamic> deserializeAsync(String value, String targetType, {bool growable = false,}) async =>
+    // ignore: deprecated_member_use_from_same_package
+    deserialize(value, targetType, growable: growable);
+
+  @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.')
+  dynamic deserialize(String value, String targetType, {bool growable = false,}) {
+    // Remove all spaces. Necessary for regular expressions as well.
+    targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments
+
+    // If the expected target type is String, nothing to do...
+    return targetType == 'String'
+      ? value
+      : fromJson(json.decode(value), targetType, growable: growable);
+  }
+{{/native_serialization}}
+
+  // ignore: deprecated_member_use_from_same_package
+  Future<String> serializeAsync(Object? value) async => serialize(value);
+
+  @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use serializeAsync() instead.')
+  String serialize(Object? value) => value == null ? '' : json.encode(value);
+
+{{#native_serialization}}
+  /// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
+  static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
+    upgradeDto(value, targetType);
+    try {
+      switch (targetType) {
+        case 'String':
+          return value is String ? value : value.toString();
+        case 'int':
+          return value is int ? value : int.parse('$value');
+        case 'double':
+          return value is double ? value : double.parse('$value');
+        case 'bool':
+          if (value is bool) {
+            return value;
+          }
+          final valueString = '$value'.toLowerCase();
+          return valueString == 'true' || valueString == '1';
+        case 'DateTime':
+          return value is DateTime ? value : DateTime.tryParse(value);
+        {{#models}}
+          {{#model}}
+        case '{{{classname}}}':
+            {{#isEnum}}
+          {{#native_serialization}}return {{{classname}}}TypeTransformer().decode(value);{{/native_serialization}}
+            {{/isEnum}}
+            {{^isEnum}}
+          return {{{classname}}}.fromJson(value);
+            {{/isEnum}}
+          {{/model}}
+        {{/models}}
+        default:
+          dynamic match;
+          if (value is List && (match = _regList.firstMatch(targetType)?.group(1)) != null) {
+            return value
+              .map<dynamic>((dynamic v) => fromJson(v, match, growable: growable,))
+              .toList(growable: growable);
+          }
+          if (value is Set && (match = _regSet.firstMatch(targetType)?.group(1)) != null) {
+            return value
+              .map<dynamic>((dynamic v) => fromJson(v, match, growable: growable,))
+              .toSet();
+          }
+          if (value is Map && (match = _regMap.firstMatch(targetType)?.group(1)) != null) {
+            return Map<String, dynamic>.fromIterables(
+              value.keys.cast<String>(),
+              value.values.map<dynamic>((dynamic v) => fromJson(v, match, growable: growable,)),
+            );
+          }
+      }
+    } on Exception catch (error, trace) {
+      throw ApiException.withInner(HttpStatus.internalServerError, 'Exception during deserialization.', error, trace,);
+    }
+    throw ApiException(HttpStatus.internalServerError, 'Could not find a suitable class for deserialization',);
+  }
+{{/native_serialization}}
+}
+{{#native_serialization}}
+
+/// Primarily intended for use in an isolate.
+class DeserializationMessage {
+  const DeserializationMessage({
+    required this.json,
+    required this.targetType,
+    this.growable = false,
+  });
+
+  /// The JSON value to deserialize.
+  final String json;
+
+  /// Target type to deserialize to.
+  final String targetType;
+
+  /// Whether to make deserialized lists or maps growable.
+  final bool growable;
+}
+
+/// Primarily intended for use in an isolate.
+Future<dynamic> decodeAsync(DeserializationMessage message) async {
+  // Remove all spaces. Necessary for regular expressions as well.
+  final targetType = message.targetType.replaceAll(' ', '');
+
+  // If the expected target type is String, nothing to do...
+  return targetType == 'String'
+    ? message.json
+    : json.decode(message.json);
+}
+
+/// Primarily intended for use in an isolate.
+Future<dynamic> deserializeAsync(DeserializationMessage message) async {
+  // Remove all spaces. Necessary for regular expressions as well.
+  final targetType = message.targetType.replaceAll(' ', '');
+
+  // If the expected target type is String, nothing to do...
+  return targetType == 'String'
+    ? message.json
+    : ApiClient.fromJson(
+        json.decode(message.json),
+        targetType,
+        growable: message.growable,
+      );
+}
+{{/native_serialization}}
+
+/// Primarily intended for use in an isolate.
+Future<String> serializeAsync(Object? value) async => value == null ? '' : json.encode(value);
diff --git a/open-api/templates/mobile/api_client.mustache.patch b/open-api/templates/mobile/api_client.mustache.patch
new file mode 100644
index 0000000000..3805cd8f79
--- /dev/null
+++ b/open-api/templates/mobile/api_client.mustache.patch
@@ -0,0 +1,10 @@
+--- api_client.mustache	2024-08-13 14:29:04.056364916 -0500
++++ api_client_new.mustache	2024-08-13 14:29:36.224410735 -0500
+@@ -159,6 +159,7 @@
+ {{#native_serialization}}
+   /// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
+   static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
++    upgradeDto(value, targetType);
+     try {
+       switch (targetType) {
+         case 'String':
diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts
index 8c50d00581..3305e1cce1 100644
--- a/server/src/dtos/user-preferences.dto.ts
+++ b/server/src/dtos/user-preferences.dto.ts
@@ -87,7 +87,7 @@ class AvatarResponse {
 }
 
 class RatingResponse {
-  enabled!: boolean;
+  enabled: boolean = false;
 }
 
 class MemoryResponse {