Handling Drift non-nullable columns and data coming from JSON

February 6, 2023

Using Drift, with the latest version of Flutter, the ID column is not nullable by default(because of the null-safety) We could make it nullable, but it’s a primary key.

  IntColumn get id => integer().autoIncrement()();

The data comes from json. fromJson returns the full row, and with null-safety, it’s not possible to not read the ID. Which is null… But we need a source for the ID column.

With the help of the following package and Drift custom row classes we can omit the ID column and allow it to be autogenerated.

  json_annotation: ^4.8.0
  json_serializable: ^6.6.1

Most posts in the issues on Github related to this question suggest using Companion.insert(body: body) In this case, we don’t have to provide the ID value to insert a record.

But this approach doesn’t allow us to use DAOs and things like insertOnConflictUpdate.

Let’s say you have the following deserializer that accepts JSON and creates a list of post companions:

class PostsDeserializer {
  final List json;
  final Set<PostsCompanion> postsCompanions;

  PostsDeserializer({required this.json}) : postsCompanions = {} {
    _call();
  }

  void _call() {
    json.forEach((postsJson) {
      postsCompanions.add(
        Post.fromJson({
          'body': postsJson['body'],
          'published_at': postsJson['published_at']
        }).toCompanion(),
      );
    });
  }
}

It’s then called with

final postsDeserializer = PostsDeserializer(json: postsRespose.data);

// and then saved with
Future _savePostsData(PostsDeserializer deserializer) async {
  deserializer.postsCompanions.forEach((companion) async {
    await db.postsDao.insertOrUpdate(companion);
  });
}

The model for the posts table may look like the following block:

import 'package:json_annotation/json_annotation.dart' as ja;

part 'posts.g.dart';

@UseRowClass(Post) // use the custom class bellow
class Posts extends Table {
  IntColumn get id => integer().autoIncrement()();
  
  TextColumn get body => text()();
  
  DateTimeColumn get publishedAt => dateTime()();
}

@ja.JsonSerializable()
class Post implements Insertable<Post> {
  final int? id;
  
  final String body;
  
  // the incoming json key has a different casing
  @ja.JsonKey(name: 'published_at')
  final String publishedAt;

  PersonService({this.id, required this.body});

  factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);

  bool get isNew => id == null;

  Map<String, dynamic> toJson() => _$PostToJson(this);

  @override
  String toString() => toJson().toString();

  PostsCompanion toCompanion() {
    if (isNew) {
      // new record - we don't have the id
      return PostCompanion.insert(body: body, createdAt: createdAt);
    } else {
      return PersonServicesCompanion(
          id: Value(id ?? 0), 
          body: Value(body), 
          createdAt: Value(createdAt));
    }
  }

  // implements Insertable
  // https://drift.simonbinder.eu/docs/advanced-features/custom_row_classes/#inserts-and-updates-with-custom-classes
  @override 
  Map<String, Expression<Object>> toColumns(bool nullToAbsent) {
    return PostsCompanion(body: Value(body), createdAt: Value(createdAt))
            .toColumns(nullToAbsent);
  }

  Post copyWith({String? body}) => Post(body: body ?? this.body);
}

After running flutter pub run build_runner build --delete-conflicting-outputs the posts.g.dart is going to look like the following:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'posts.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Post _$PostFromJson(Map<String, dynamic> json) => Post(
      id: json['id'] as int?,
      body: json['body'] as String,
      publishedAt: DateTime.parse(json['published_at'] as String),
    );
	
Map<String, dynamic> _$PostToJson(Post instance) => <String, dynamic>{
      'id': instance.id,
      'body': instance.body,
      'published_at': instance.publishedAt,
    };
	
comments powered by Disqus