Docs
Functions
Defining custom data types

Defining custom data types

With Celest Functions, you can use any of the core Dart types available such as int, String, and DateTime. You can also use your custom data types. Celest will handle the transfer and formatting of data from your Flutter app to your backend, also known as serialization, out-of-the-box in most cases. In situations requiring custom serialization, you can write your logic which will be used instead.

Celest does not support the following Dart types as parameter or return types. Object and dynamic may only be used as the value type of a Map.

  • Stream
  • Object
  • dynamic

Custom data type example

Imagine you're working on an e-commerce application with an Order class defined in your codebase. To have Celest use that custom class, you need to place it in the <flutter_app>/celest/lib/models/ folder.

// celest/lib/models/order.dart
 
class Order {
  const Order ({
    required this.id,
    required this.customerName,
    required this.price,
  });
 
  final int id;
  final String customerName;
  final Price price;
}
 
enum Currency { usd, cad, ... }
 
class Price {
  const Price({
    required this.currency,
    required this.dollars,
    required this.cents,
  }): assert(cents < 100);
 
  final Currency currency;
  final int dollars;
  final int cents;
}

Use this Order type in any Celest Function as either a parameter or return value, without adding serialization logic.

// celest/functions/orders.dart
 
import 'package:celest_backend/models/order.dart';
 
Future<String> createOrder(
  Order customerOrder,
) async {
  // Function logic goes here
}

When communicating with your backend, Celest will serialize the Order class as a JSON map with the field names as keys. The resulting information transferred over to your backend will have the following shape.

{
  "customerOrder": {
    "id": 123,
    "customerName": "Celest",
    "price": {
      "currency": "usd",
      "dollars": 100,
      "cents": 34
    }
  }
}

Writing custom serialization logic

If you need custom handling for your serialization logic, add the fromJson and toJson methods to your data type. Celest will use your custom fromJson/toJson implementations when transmitting the type to and from your backend.

Here, the Price.toJson method is used to upper-case the currency value.

// celest/lib/models/order.dart
 
class Price {
  // ...
 
  factory Price.fromJson(Map<String, dynamic> json) {
    // ...
  }
 
  Map<String, dynamic> toJson() => {
      'currency': currency.name.toUpperCase(),
      'dollars': dollars,
      'cents': cents,
    };
}

The resulting JSON response for the currency will now be returned as upper case.

{
 "customerOrder": {
   "id": 123,
   "customerName": "Celest",
   "price": {
+    "currency": "USD",
-    "currency": "usd",
     "dollars": 100,
     "cents": 34
   }
 }
}

Overriding serialization for third-party types

Sometimes, though, you cannot control the fromJson/toJson methods of a class, such as when using a third-party library. In these cases, you can use a custom override to "redefine" the type for serialization in Celest.

For example, consider the case where the Price class from our Order type is imported from a third-party library, package:price. Since we do not own package:price, we cannot change the fromJson and toJson methods of the Price class.

Instead of modifying the Price class, we can define a custom override for Price which will apply to all instances of the Price class when it's encountered during serialization.

import 'package:price/price.dart';
 
@override
extension type PriceOverride(Price price) {
  factory PriceOverride.fromJson(Map<String, Object?> json) {
    return PriceOverride(
      Price(
        currency: Currency.values.firstWhere(
          (e) => e.toString().toUpperCase() == json['currency'],
        ),
        dollars: json['dollars'] as int,
        cents: json['cents'] as int,
      ),
    );
  }
 
  Map<String, Object?> toJson() => {
        'currency': price.currency.toString().toUpperCase(),
        'dollars': price.dollars,
        'cents': price.cents,
      };
}

Next steps

You have now learned how Celest handles the serialization of requests/responses to your functions, and how to use your own custom types and serialization logic. Learn about more features of Celest Functions by following our guides for defining custom exceptions and managing environment variables.