I Thought This Was Private… Again! But Freezed Had a Different Story

Md. Al - AminMd. Al - Amin
4 min read

It was 10:30 PM on a typical night. Instead of winding down, I was tuned into a bi-weekly Flutter session hosted by the Flutter Bangladesh Group, led by none other than Momshad Dinury bhai, the group’s admin and a Senior Software Engineer at Brain Station 23.

That night’s topic?
Freezed package’s latest major update and the breaking changes it introduced.

Honestly, I was there out of curiosity. I’ve used freezed in several projects, and I love how it handles immutability, union types, and copyWith. But that night, I stumbled onto something I didn’t expect… again.

Freezed’s Major Update: What Changed?

Momshad bhai shared that freezed now requires a keyword like sealed or abstract when defining classes using the factory constructor.

Here’s the change:

@freezed
-class Person with _$Person {
+abstract class Person with _$Person {
  const factory Person({
    required String firstName,
    required String lastName,
    required int age,
  }) = _Person;

  factory Person.fromJson(Map<String, Object?> json)
      => _$PersonFromJson(json);
}

Or for union types:

@freezed
-class Model with _$Model {
+sealed class Model with _$Model {
  factory Model.first(String a) = First;
  factory Model.second(int b, bool c) = Second;
}

This migration was required because of changes in Dart itself. But as I followed the example, something caught my attention:

@freezed
abstract class Person with _$Person {
  const factory Person({
    required String firstName,
    required String lastName,
    required int age,
  }) = _Person;

  factory Person.fromJson(Map<String, Object?> json) => _$PersonFromJson(json);
}

Wait… _Person and _$PersonFromJson?

They start with an underscore aren’t those private?
Yet, they’re accessible right here in the same file.

My mind instantly went back to something I had written not long ago: I Thought This Was Private… Until Dart Surprised Me

Dart Privacy Refresher: Underscore Isn’t File-Private

If you’ve used Dart for a while, you know that prefixing a class or variable with _ makes it private.

But what kind of private?

Here’s the twist: Dart’s privacy is scoped to the library, not the file.

That means as long as your files are part of the same library, they can share each other’s private members.

Enter part and part of: The Glue Behind Code Generation

Here’s how this works behind the scenes:

In your Dart file (person.dart), you’ll see:

part 'person.freezed.dart';
part 'person.g.dart';

Inside those generated files (like person.freezed.dart), you’ll find:

part of 'person.dart';

This tells Dart: “Hey, all these files are part of the same library.”
So suddenly, _Person and _$PersonFromJson, even though private by naming convention, are accessible.

Mind = Blown

What Are _$Person and _Person Anyway?

Here’s the breakdown of what freezed generates:

  • _$Person: A mixin or helper generated by freezed, giving you the .copyWith(), .toString(), .hashCode, etc.

  • _Person: The actual implementation class behind your factory constructor.

  • _$PersonFromJson(): A function created by json_serializable to handle deserialization.

All of this becomes possible due to part and part of.

New Dart Feature: Pattern Matching

One more thing I learned from that session: freezed no longer generates map() or when() methods for pattern matching.

Now, you’re expected to use Dart’s native pattern matching syntax. For example:

final model = Model.first('42');

final result = switch (model) {
  First(:final a) => 'first $a',
  Second(:final b, :final c) => 'second $b $c',
};

No more model.map() magic. This aligns better with modern Dart, but it also means we must adjust our habits.

But why abstract or sealed Now?

This was a change enforced by Dart itself.

When using factory constructors, Dart now expects you to mark the class as either:

  • abstract: Meaning it cannot be instantiated directly.

  • sealed: Meaning it can only be subclassed within the same file great for union types and pattern matching.

So, this change is less about freezed itself and more about keeping up with Dart’s stricter and cleaner type system.

Key Takeaways

Here’s what I walked away with from a 10:30 PM session that was supposed to be “just another meetup”:

  • Dart’s privacy is library-scoped, not file-scoped.

  • part and part of allow generated code to access and expose private classes across files.

  • freezed’s latest update aligns with modern Dart features like sealed, abstract, and native pattern matching.

  • Code generation feels magical but understanding the “why” behind the magic makes you a better dev.

Final Thoughts

It’s wild how small moments like a late-night tech session can lead to big learning.

What looked like an accessibility bug (_Person, _$Person) was actually a deep Dart feature.
And what felt like a confusing migration (sealed, abstract) was Dart evolving into a cleaner, more expressive language.

Big thanks to Momshad Dinury bhai and the Flutter Bangladesh Group for organizing these sessions.

They don’t just teach you tools they lead you into deeper understanding.

If you liked this, you might also enjoy my earlier post: I Thought This Was Private… Until Dart Surprised Me

Was this helpful? Drop a comment or share your own “Dart surprise” moment I’d love to hear it.

Happy Fluttering!

0
Subscribe to my newsletter

Read articles from Md. Al - Amin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Md. Al - Amin
Md. Al - Amin

Experienced Android Developer with a demonstrated history of working for the IT industry. Skilled in JAVA, Dart, Flutter, and Teamwork. Strong Application Development professional with a Bachelor's degree focused in Computer Science & Engineering from Daffodil International University-DIU.