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

Table of contents
- Freezed’s Major Update: What Changed?
- Dart Privacy Refresher: Underscore Isn’t File-Private
- Enter part and part of: The Glue Behind Code Generation
- What Are _$Person and _Person Anyway?
- New Dart Feature: Pattern Matching
- But why abstract or sealed Now?
- Key Takeaways
- Final Thoughts
- Was this helpful? Drop a comment or share your own “Dart surprise” moment I’d love to hear it.

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 byfreezed
, giving you the.copyWith()
,.toString()
,.hashCode
, etc._Person
: The actual implementation class behind your factory constructor._$PersonFromJson()
: A function created byjson_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
andpart of
allow generated code to access and expose private classes across files.freezed
’s latest update aligns with modern Dart features likesealed
,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!
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.