Address further review comments, further cleanup and add comments.

Made any classes and methods that can be private, private.
diff --git a/pkgs/dart_model/lib/src/json_buffer/closed_map.dart b/pkgs/dart_model/lib/src/json_buffer/closed_map.dart
index 0c38e02..4cfc983 100644
--- a/pkgs/dart_model/lib/src/json_buffer/closed_map.dart
+++ b/pkgs/dart_model/lib/src/json_buffer/closed_map.dart
@@ -18,12 +18,12 @@
   static const _valueSize = _typeSize + _pointerSize;
   static const _entrySize = _keySize + _valueSize;
 
-  /// Adds a `Map` to the buffer, returns the [Pointer] to it.
+  /// Adds a `Map` to the buffer, returns the [_Pointer] to it.
   ///
   /// The `Map` should be small and already evaluated, making it fast to
   /// iterate. For large maps see "growable map" methods.
-  Pointer _addClosedMap(Map<String, Object?> map) {
-    explanations?.push('addClosedMap $map');
+  _Pointer _addClosedMap(Map<String, Object?> map) {
+    _explanations?.push('addClosedMap $map');
 
     final length = map.length;
     final pointer = _reserve(_lengthSize + length * _entrySize);
@@ -39,12 +39,12 @@
       entryPointer += _entrySize;
     }
 
-    explanations?.pop();
+    _explanations?.pop();
     return pointer;
   }
 
   /// Returns the [_ClosedMap] at [pointer].
-  Map<String, Object?> readClosedMap(Pointer pointer) {
+  Map<String, Object?> _readClosedMap(_Pointer pointer) {
     return _ClosedMap(this, pointer);
   }
 }
@@ -52,12 +52,12 @@
 class _ClosedMap
     with MapMixin<String, Object?>, _EntryMapMixin<String, Object?> {
   final JsonBufferBuilder _buffer;
-  final Pointer _pointer;
+  final _Pointer _pointer;
   @override
   final int length;
 
   _ClosedMap(this._buffer, this._pointer)
-      : length = _buffer.readUint32(_pointer);
+      : length = _buffer._readLength(_pointer);
 
   @override
   Object? operator [](Object? key) {
@@ -86,28 +86,31 @@
 
   @override
   void operator []=(String key, Object? value) {
-    throw UnsupportedError('ClosedMap is readonly.');
+    throw UnsupportedError(
+        'This JsonBufferBuilder map is read-only, see "createGrowableMap".');
   }
 
   @override
   Object? remove(Object? key) {
-    throw UnsupportedError('ClosedMap is readonly.');
+    throw UnsupportedError(
+        'This JsonBufferBuilder map is read-only, see "createGrowableMap".');
   }
 
   @override
   void clear() {
-    throw UnsupportedError('ClosedMap is readonly.');
+    throw UnsupportedError(
+        'This JsonBufferBuilder map is read-only, see "createGrowableMap".');
   }
 }
 
 /// `Iterator` that reads a "closed map" in a [JsonBufferBuilder].
 abstract class _ClosedMapIterator<T> implements Iterator<T> {
   final JsonBufferBuilder _buffer;
-  final Pointer _last;
+  final _Pointer _last;
 
-  Pointer _pointer;
+  _Pointer _pointer;
 
-  _ClosedMapIterator(this._buffer, Pointer pointer, int length)
+  _ClosedMapIterator(this._buffer, _Pointer pointer, int length)
       : _last = pointer + _lengthSize + length * ClosedMaps._entrySize,
         // Subtract because `moveNext` is called before reading.
         _pointer = pointer + _lengthSize - ClosedMaps._entrySize;
@@ -115,7 +118,7 @@
   @override
   T get current;
 
-  String get _currentKey => _buffer.readString(_buffer.readPointer(_pointer));
+  String get _currentKey => _buffer._readString(_buffer._readPointer(_pointer));
   Object? get _currentValue => _buffer._readAny(_pointer + _pointerSize);
 
   @override
diff --git a/pkgs/dart_model/lib/src/json_buffer/explanations.dart b/pkgs/dart_model/lib/src/json_buffer/explanations.dart
index 117bcfd..87839d9 100644
--- a/pkgs/dart_model/lib/src/json_buffer/explanations.dart
+++ b/pkgs/dart_model/lib/src/json_buffer/explanations.dart
@@ -4,20 +4,9 @@
 
 part of 'json_buffer_builder.dart';
 
-/// Global hook for turning on "debug logs" for [JsonBufferBuilder].
-///
-/// Set to an instance of [Explanations] to turn on logging, reset to null to
-/// turn off logging. If set, [JsonBufferBuilder#toString] will print
-/// additional information for each byte.
-///
-/// TODO(davidmorgan): this is only intended for use in local unit tests.
-/// If debug logs are useful elsewhere, do something more robust, but take
-/// care not to degrade performance when logs are off.
-Explanations? explanations;
-
 /// Tracks why each byte in a [JsonBufferBuilder] was written, and does
 /// additional checks.
-class Explanations {
+class _Explanations {
   final Map<int, String> _explanationsByPointer = {};
   final List<String> _explanationsStack = [];
 
@@ -44,7 +33,7 @@
   ///
   /// Set [allowOverwrite] if multiple writes to this location are allowed.
   /// Otherwise, this method throws on multiple writes.
-  void explain(Pointer pointer, {bool allowOverwrite = false}) {
+  void explain(_Pointer pointer, {bool allowOverwrite = false}) {
     if (_explanationsStack.isNotEmpty) {
       final explanation = _explanationsStack.join(', ') +
           (allowOverwrite ? _allowOverwriteTag : '');
@@ -61,7 +50,7 @@
   }
 
   /// Associate the current explanation stack with all bytes [from] until [to].
-  void explainRange(Pointer from, Pointer to) {
+  void explainRange(_Pointer from, _Pointer to) {
     for (var pointer = from; pointer != to; ++pointer) {
       explain(pointer);
     }
diff --git a/pkgs/dart_model/lib/src/json_buffer/growable_map.dart b/pkgs/dart_model/lib/src/json_buffer/growable_map.dart
index 39229b0..06a4fae 100644
--- a/pkgs/dart_model/lib/src/json_buffer/growable_map.dart
+++ b/pkgs/dart_model/lib/src/json_buffer/growable_map.dart
@@ -38,19 +38,19 @@
   /// somewhere in the buffer. Otherwise, it won't be reachable from the
   /// root [map].
   Map<String, V> createGrowableMap<V>() {
-    explanations?.push('addGrowableMap');
+    _explanations?.push('addGrowableMap');
     final pointer = _reserve(_pointerSize + _lengthSize);
     // Initially a "growable map" is just a null pointer and zero size, so
     // there is nothing to write.
-    explanations?.pop();
+    _explanations?.pop();
     return _readGrowableMap<V>(pointer);
   }
 
-  /// Returns the [Pointer] to [map].
+  /// Returns the [_Pointer] to [map].
   ///
   /// The [map] must have been created in this buffer using
   /// [createGrowableMap]. Otherwise, [UnsupportedError] is thrown.
-  Pointer _pointerToGrowableMap(_GrowableMap<Object?> map) {
+  _Pointer _pointerToGrowableMap(_GrowableMap<Object?> map) {
     _checkGrowableMapOwnership(map);
     return map._pointer;
   }
@@ -58,25 +58,25 @@
   /// Throws if [map is backed by a different buffer to `this`.
   void _checkGrowableMapOwnership(_GrowableMap map) {
     if (map._buffer != this) {
-      throw UnsupportedError('Maps created with `addGrowableMap` can only '
+      throw UnsupportedError('Maps created with `createGrowableMap` can only '
           'be added to the JsonBufferBuilder instance that created them.');
     }
   }
 
   /// Returns the [_GrowableMap] at [pointer].
-  Map<String, V> _readGrowableMap<V>(Pointer pointer) {
+  Map<String, V> _readGrowableMap<V>(_Pointer pointer) {
     return _GrowableMap<V>(this, pointer);
   }
 }
 
 class _GrowableMap<V> with MapMixin<String, V>, _EntryMapMixin<String, V> {
   final JsonBufferBuilder _buffer;
-  final Pointer _pointer;
+  final _Pointer _pointer;
   int _length;
-  Pointer? _lastPointer;
+  _Pointer? _lastPointer;
 
   _GrowableMap(this._buffer, this._pointer)
-      : _length = _buffer.readLength(_pointer + _pointerSize);
+      : _length = _buffer._readLength(_pointer + _pointerSize);
 
   @override
   int get length => _length;
@@ -115,7 +115,7 @@
   /// iterable.
   @override
   void operator []=(String key, V value) {
-    explanations?.push('GrowableMap[]= $key $value');
+    _buffer._explanations?.push('GrowableMap[]= $key $value');
 
     // If `_lastPointer` is not set yet, walk the map to find the end of it.
     if (_lastPointer == null) {
@@ -140,7 +140,7 @@
     // Update length.
     ++_length;
     _buffer._writeLength(_pointer + _pointerSize, length, allowOverwrite: true);
-    explanations?.pop();
+    _buffer._explanations?.pop();
   }
 
   @override
@@ -157,7 +157,7 @@
 /// `Iterator` that reads a "growable map" in a [JsonBufferBuilder].
 abstract class _GrowableMapIterator<T> implements Iterator<T> {
   final JsonBufferBuilder _buffer;
-  Pointer _pointer;
+  _Pointer _pointer;
 
   _GrowableMapIterator(this._buffer, this._pointer);
 
@@ -165,13 +165,13 @@
   T get current;
 
   String get _currentKey =>
-      _buffer.readString(_buffer.readPointer(_pointer + _pointerSize));
+      _buffer._readString(_buffer._readPointer(_pointer + _pointerSize));
   Object? get _currentValue =>
       _buffer._readAny(_pointer + _pointerSize + GrowableMaps._keySize);
 
   @override
   bool moveNext() {
-    _pointer = _buffer.readPointer(_pointer);
+    _pointer = _buffer._readPointer(_pointer);
     return _pointer != 0;
   }
 }
diff --git a/pkgs/dart_model/lib/src/json_buffer/iterables.dart b/pkgs/dart_model/lib/src/json_buffer/iterables.dart
index d85385e..21ef60a 100644
--- a/pkgs/dart_model/lib/src/json_buffer/iterables.dart
+++ b/pkgs/dart_model/lib/src/json_buffer/iterables.dart
@@ -5,7 +5,7 @@
 part of 'json_buffer_builder.dart';
 
 /// More efficient implementations than `MapMixin` for maps with efficient
-/// `entries`.
+/// `entries` and `length`.
 mixin _EntryMapMixin<K, V> on Map<K, V> {
   // `MapMixin` iterates keys then looks up each value.
   //
@@ -16,6 +16,14 @@
       action(entry.key, entry.value);
     }
   }
+
+  // `MapMixin` uses `keys.isEmpty`.
+  @override
+  bool get isEmpty => length == 0;
+
+// `MapMixin` uses `keys.isNotEmpty`.
+  @override
+  bool get isNotEmpty => length != 0;
 }
 
 /// An [Iterable] that uses the supplied function to create an [Iterator].
diff --git a/pkgs/dart_model/lib/src/json_buffer/json_buffer_builder.dart b/pkgs/dart_model/lib/src/json_buffer/json_buffer_builder.dart
index 9cd12e6..c42b85c 100644
--- a/pkgs/dart_model/lib/src/json_buffer/json_buffer_builder.dart
+++ b/pkgs/dart_model/lib/src/json_buffer/json_buffer_builder.dart
@@ -27,10 +27,16 @@
   /// Whether writes are allowed.
   final bool _allowWrites;
 
-  final Map<TypedMapSchema, Pointer> _pointersBySchema = Map.identity();
-  final Map<Pointer, TypedMapSchema> _schemasByPointer = {};
+  /// [_Pointer]s to [TypedMapSchema]s used by [_TypedMap]s, so that the
+  /// same schema is only written once.
+  final Map<TypedMapSchema, _Pointer> _pointersBySchema = Map.identity();
 
-  final Map<String, Pointer> _pointersByString = {};
+  /// [TypedMapSchema] used by [_TypedMap]s by [_Pointer], so that the same
+  /// schema is only read once.
+  final Map<_Pointer, TypedMapSchema> _schemasByPointer = {};
+
+  /// [_Pointer]s to `String`s, so that the same `String` is only written once.
+  final Map<String, _Pointer> _pointersByString = {};
 
   JsonBufferBuilder.deserialize(this._buffer)
       : _allowWrites = false,
@@ -54,44 +60,44 @@
   /// The number of bytes written.
   int get length => _nextFree;
 
-  /// Reads the value at [Pointer], which must have been written with
+  /// Reads the value at [_Pointer], which must have been written with
   /// [_writeAny].
-  Object? _readAny(Pointer pointer) {
-    final type = readType(pointer);
+  Object? _readAny(_Pointer pointer) {
+    final type = _readType(pointer);
     return _read(type, pointer + _typeSize);
   }
 
-  /// Reads the value of type [Type] at [Pointer].
-  Object? _read(Type type, Pointer pointer) {
+  /// Reads the value of type [Type] at [_Pointer].
+  Object? _read(Type type, _Pointer pointer) {
     switch (type) {
       case Type.nil:
         return null;
       case Type.type:
-        return readType(pointer);
+        return _readType(pointer);
       case Type.pointer:
-        return readPointer(pointer);
+        return _readPointer(pointer);
       case Type.uint32:
-        return readUint32(pointer);
+        return _readUint32(pointer);
       case Type.boolean:
-        return readBoolean(pointer);
+        return _readBoolean(pointer);
       case Type.stringPointer:
-        return readString(readPointer(pointer));
+        return _readString(_readPointer(pointer));
       case Type.closedMapPointer:
-        return readClosedMap(readPointer(pointer));
+        return _readClosedMap(_readPointer(pointer));
       case Type.growableMapPointer:
-        return _readGrowableMap<Object?>(readPointer(pointer));
+        return _readGrowableMap<Object?>(_readPointer(pointer));
       case Type.typedMapPointer:
-        return readTypedMap(readPointer(pointer));
+        return _readTypedMap(_readPointer(pointer));
     }
   }
 
   /// Writes the type of [value] then writes the value using [_writeAnyOfType].
-  void _writeAny(Pointer pointer, Object? value) {
-    explanations?.push('_writeAny $pointer $value');
+  void _writeAny(_Pointer pointer, Object? value) {
+    _explanations?.push('_writeAny $pointer $value');
     final type = Type._of(value);
     _writeType(pointer, type);
     _writeAnyOfType(type, pointer + _typeSize, value);
-    explanations?.pop();
+    _explanations?.pop();
   }
 
   /// Writes [value] of type [type] to [pointer].
@@ -102,8 +108,8 @@
   ///
   /// Otherwise, a pointer is written. It might be a pointer to an existing
   /// value if there is one, or a new value may be written as well.
-  void _writeAnyOfType(Type type, Pointer pointer, Object? value) {
-    explanations?.push('_writeAnyOfType $type $pointer $value');
+  void _writeAnyOfType(Type type, _Pointer pointer, Object? value) {
+    _explanations?.push('_writeAnyOfType $type $pointer $value');
     switch (type) {
       case Type.nil:
         // Nothing to write.
@@ -113,7 +119,7 @@
         _writeType(pointer, value as Type);
 
       case Type.pointer:
-        _writePointer(pointer, value as Pointer);
+        _writePointer(pointer, value as _Pointer);
 
       case Type.uint32:
         _writeUint32(pointer, value as int);
@@ -134,95 +140,107 @@
       case Type.typedMapPointer:
         _writePointer(pointer, _pointerToTypedMap(value as _TypedMap));
     }
-    explanations?.pop();
+    _explanations?.pop();
   }
 
-  /// Returns a [Pointer] to the `String` [value].
+  /// Returns a [_Pointer] to the `String` [string].
   ///
-  /// Returns the [Pointer] of an existing equal `String` if there is one,
+  /// Returns the [_Pointer] of an existing equal `String` if there is one,
   /// otherwise adds it.
-  Pointer _pointerToString(String value) =>
-      _pointersByString[value] ??= _addString(value);
+  _Pointer _pointerToString(String string) =>
+      _pointersByString[string] ??= _addString(string);
 
-  Pointer _addString(String value) {
-    explanations?.push('__pointerToString $value');
-    final bytes = utf8.encode(value);
+  /// Adds the `String` [string], returns a [_Pointer] to it.
+  _Pointer _addString(String string) {
+    _explanations?.push('__pointerToString $string');
+    final bytes = utf8.encode(string);
     final length = bytes.length;
     final pointer = _reserve(_lengthSize + length);
     _writeLength(pointer, length);
-    _setRange(pointer + 4, pointer + 4 + length, bytes);
-    explanations?.pop();
+    _setRange(pointer + _lengthSize, pointer + _lengthSize + length, bytes);
+    _explanations?.pop();
     return pointer;
   }
 
-  void _writeType(Pointer pointer, Type value) {
-    explanations?.push('_writeType $value');
-    _setByte(pointer, value.index);
-    explanations?.pop();
+  /// Reads the String at [pointer].
+  String _readString(_Pointer pointer) {
+    final length = _readLength(pointer);
+    return utf8.decode(
+        _buffer.sublist(pointer + _lengthSize, pointer + _lengthSize + length));
   }
 
-  Type readType(Pointer pointer) {
+  /// Writes [type] at [pointer].
+  void _writeType(_Pointer pointer, Type type) {
+    _explanations?.push('_writeType $type');
+    _setByte(pointer, type.index);
+    _explanations?.pop();
+  }
+
+  /// Reads the `Type` at [pointer].
+  Type _readType(_Pointer pointer) {
     return Type.values[_buffer[pointer]];
   }
 
-  void _writeLength(Pointer pointer, Pointer value,
+  /// Writes [length] at [pointer].
+  void _writeLength(_Pointer pointer, int length,
       {bool allowOverwrite = false}) {
-    explanations?.push('_writeLength $value');
-    __writeUint32(pointer, value, allowOverwrite: allowOverwrite);
-    explanations?.pop();
+    _explanations?.push('_writeLength $length');
+    __writeUint32(pointer, length, allowOverwrite: allowOverwrite);
+    _explanations?.pop();
   }
 
-  /// Adds a new pointer pointing to [pointer], returns it.
-  Pointer _addPointerTo(Pointer pointer) {
-    explanations?.push('_addPointerToPointer $pointer');
-    final result = _reserve(4);
+  /// Reads the length at [_Pointer].
+  _Pointer _readLength(_Pointer pointer) => _readUint32(pointer);
+
+  /// Adds [pointer] to the buffer, returns a new [_Pointer] to it.
+  _Pointer _addPointerTo(_Pointer pointer) {
+    _explanations?.push('_addPointerToPointer $pointer');
+    final result = _reserve(_pointerSize);
     _writePointer(result, pointer);
-    explanations?.pop();
+    _explanations?.pop();
     return result;
   }
 
-  void _writePointer(Pointer pointer, Pointer value) {
-    explanations?.push('_writePointer $value');
-    __writeUint32(pointer, value);
-    explanations?.pop();
+  /// Writes [pointerValue] at [pointer].
+  void _writePointer(_Pointer pointer, _Pointer pointerValue) {
+    _explanations?.push('_writePointer $pointerValue');
+    __writeUint32(pointer, pointerValue);
+    _explanations?.pop();
   }
 
-  void _writeUint32(Pointer pointer, int value) {
-    explanations?.push('_writeUint32 $value');
+  /// Reads the [_Pointer] at [_Pointer].
+  _Pointer _readPointer(_Pointer pointer) => _readUint32(pointer);
+
+  /// Writes [value] at [pointer].
+  void _writeUint32(_Pointer pointer, int value) {
+    _explanations?.push('_writeUint32 $value');
     __writeUint32(pointer, value);
-    explanations?.pop();
+    _explanations?.pop();
   }
 
-  void __writeUint32(Pointer pointer, int value,
+  void __writeUint32(_Pointer pointer, int value,
       {bool allowOverwrite = false}) {
     _setFourBytes(pointer, value & 0xff, (value >> 8) & 0xff,
         (value >> 16) & 0xff, (value >> 24) & 0xff,
         allowOverwrite: allowOverwrite);
   }
 
-  void _writeBoolean(Pointer pointer, bool value) {
-    explanations?.push('_writeBoolean $value');
-    _setByte(pointer, value ? 1 : 0);
-    explanations?.pop();
-  }
-
-  /// Reads the length at [Pointer].
-  Pointer readLength(Pointer pointer) => _readUint32(pointer);
-
-  /// Reads the [Pointer] at [Pointer].
-  Pointer readPointer(Pointer pointer) => _readUint32(pointer);
-
-  /// Reads the uint32 at [Pointer].
-  int readUint32(Pointer pointer) => _readUint32(pointer);
-
-  int _readUint32(Pointer pointer) =>
+  /// Reads the uint32 at [_Pointer].
+  int _readUint32(_Pointer pointer) =>
       _buffer[pointer] +
       (_buffer[pointer + 1] << 8) +
       (_buffer[pointer + 2] << 16) +
       (_buffer[pointer + 3] << 24);
 
-  /// Reads the boolean at [Pointer].
-  bool readBoolean(Pointer pointer) {
+  /// Writes [boolean] at [pointer].
+  void _writeBoolean(_Pointer pointer, bool boolean) {
+    _explanations?.push('_writeBoolean $boolean');
+    _setByte(pointer, boolean ? 1 : 0);
+    _explanations?.pop();
+  }
+
+  /// Reads the `bool` at [_Pointer].
+  bool _readBoolean(_Pointer pointer) {
     switch (_buffer[pointer]) {
       case 0:
         return false;
@@ -233,34 +251,26 @@
     }
   }
 
-  /// Reads the String at [Pointer].
-  String readString(Pointer pointer) {
-    final length = readLength(pointer);
-    return utf8.decode(
-        _buffer.sublist(pointer + _lengthSize, pointer + _lengthSize + length));
-  }
-
-  void _setBit(Pointer pointer, int bitIndex, bool value) {
-    _checkAllowWrites();
-    explanations?.explain(pointer, allowOverwrite: true);
-    if (value) {
-      _buffer[pointer] |= 1 << bitIndex;
-    } else {
-      _buffer[pointer] &= 0xff ^ (1 << bitIndex);
-    }
-  }
-
-  bool readBit(Pointer pointer, int bitIndex) {
+  /// Reads the bit at [pointer], index [bitIndex].
+  bool _readBit(_Pointer pointer, int bitIndex) {
     return ((_buffer[pointer] >> bitIndex) & 1) == 1;
   }
 
-  void _setByte(Pointer pointer, int uint8, {bool allowOverwrite = false}) {
+  /// Sets the byte at [pointer] to [uint8].
+  ///
+  /// If [_explanations] is being used, [allowOverwrite] controls whether
+  /// multiple writes to the same byte will be allowed or will throw.
+  void _setByte(_Pointer pointer, int uint8, {bool allowOverwrite = false}) {
     _checkAllowWrites();
-    explanations?.explain(pointer, allowOverwrite: allowOverwrite);
+    _explanations?.explain(pointer, allowOverwrite: allowOverwrite);
     _buffer[pointer] = uint8;
   }
 
-  void _setFourBytes(Pointer pointer, int b1, int b2, int b3, int b4,
+  /// Sets the four bytes at [pointer] to [b1] [b2] [b3] [b4].
+  ///
+  /// If [_explanations] is being used, [allowOverwrite] controls whether
+  /// multiple writes to the same byte will be allowed or will throw.
+  void _setFourBytes(_Pointer pointer, int b1, int b2, int b3, int b4,
       {bool allowOverwrite = false}) {
     _setByte(pointer, b1, allowOverwrite: allowOverwrite);
     _setByte(pointer + 1, b2, allowOverwrite: allowOverwrite);
@@ -268,9 +278,10 @@
     _setByte(pointer + 3, b4, allowOverwrite: allowOverwrite);
   }
 
-  void _setRange(Pointer from, Pointer to, Uint8List bytes) {
+  /// Sets the rang of bytes [from] until [to] to [bytes].
+  void _setRange(_Pointer from, _Pointer to, Uint8List bytes) {
     _checkAllowWrites();
-    explanations?.explainRange(from, to);
+    _explanations?.explainRange(from, to);
     _buffer.setRange(from, to, bytes);
   }
 
@@ -278,8 +289,8 @@
   ///
   /// Increases `_nextFree` accordingly. Expands the buffer if necessary.
   ///
-  /// Returns a [Pointer] to the reserved space.
-  Pointer _reserve(int bytes) {
+  /// Returns a [_Pointer] to the reserved space.
+  _Pointer _reserve(int bytes) {
     _checkAllowWrites();
     final result = _nextFree;
     _nextFree += bytes;
@@ -297,17 +308,27 @@
     _buffer.setRange(0, oldBuffer.length, oldBuffer);
   }
 
+  /// Throws if writes are not allowed.
   void _checkAllowWrites() {
     if (!_allowWrites) throw StateError('This JsonBufferBuilder is read-only.');
   }
 
+  /// Explanations of each byte in the buffer.
+  ///
+  /// Run tests with `dart -Ddebug_json_buffer=true test -c source` to enable.
+  ///
+  /// Then, [JsonBufferBuilder#toString] prints additional information for
+  /// each byte.
+  final _Explanations? _explanations =
+      const bool.fromEnvironment('debug_json_buffer') ? _Explanations() : null;
+
   @override
   String toString() {
-    if (explanations == null) return 'JsonBufferBuilder($_nextFree, $_buffer)';
+    if (_explanations == null) return 'JsonBufferBuilder($_nextFree, $_buffer)';
     final result = StringBuffer('JsonBufferBuilder($_nextFree):\n');
     for (var i = 0; i != _nextFree; ++i) {
       final value = _buffer[i];
-      final explanation = explanations!._explanationsByPointer[i];
+      final explanation = _explanations._explanationsByPointer[i];
       if (explanation == null) {
         result.writeln('$i: $value');
       } else {
diff --git a/pkgs/dart_model/lib/src/json_buffer/type.dart b/pkgs/dart_model/lib/src/json_buffer/type.dart
index 2ae626b..a7c3ece 100644
--- a/pkgs/dart_model/lib/src/json_buffer/type.dart
+++ b/pkgs/dart_model/lib/src/json_buffer/type.dart
@@ -18,8 +18,10 @@
 /// TODO(davidmorgan): this matters for performance; optimize.
 const int _initialBufferSize = 32;
 
-typedef Pointer = int;
+/// A pointer into the buffer.
+typedef _Pointer = int;
 
+/// The type of a value in the buffer.
 enum Type {
   nil,
   type,
@@ -31,6 +33,7 @@
   growableMapPointer,
   typedMapPointer;
 
+  /// Returns the [Type] of [value], or throws if it is not a supported type.
   static Type _of(Object? value) {
     switch (value) {
       case Null():
@@ -54,8 +57,8 @@
         'Unsupported type: ${value.runtimeType}, value: $value');
   }
 
-  /// Returns size in bytes.
-  int get sizeInBytes {
+  /// The size in bytes of value of this type.
+  int get _sizeInBytes {
     switch (this) {
       case nil:
         return 0;
diff --git a/pkgs/dart_model/lib/src/json_buffer/typed_map.dart b/pkgs/dart_model/lib/src/json_buffer/typed_map.dart
index 2233f78..cd51166 100644
--- a/pkgs/dart_model/lib/src/json_buffer/typed_map.dart
+++ b/pkgs/dart_model/lib/src/json_buffer/typed_map.dart
@@ -4,23 +4,37 @@
 
 part of 'json_buffer_builder.dart';
 
+/// Schema of a "typed map": a map with known keys and their value types.
 class TypedMapSchema {
+  /// The `String` keys of a "typed map" using this schema.
   final List<String> _keys;
-  final List<Type> _valueTypes;
-  final List<int> _offsets;
-  final bool isAllBooleans;
 
+  /// The value types of the fields in a "typed map" using this schema.
+  final List<Type> _valueTypes;
+
+  /// Whether all the fields are of type [Type.boolean].
+  final bool _isAllBooleans;
+
+  /// The number of bytes needed for a bit vector of which fields are present.
+  final int _fieldSetSize;
+
+  /// The byte size of all values if bools are not packed to bit vectors.
+  final int _valueSizeAsBytes;
+
+  /// Schema with the specified [fieldTypes].
+  ///
+  /// Ordering is important: when a "typed map" is instantiated the values are
+  /// passed in the same order that the fields are specified here.
   TypedMapSchema(Map<String, Type> fieldTypes)
       : this._(fieldTypes.keys.toList(), fieldTypes.values.toList());
 
   TypedMapSchema._(this._keys, this._valueTypes)
-      : _offsets = List<int>.filled(_keys.length + 1, 0),
-        isAllBooleans = _valueTypes.every((t) => t == Type.boolean) {
-    for (var i = 1; i != _valueTypes.length + 1; ++i) {
-      _offsets[i] = _offsets[i - 1] + _valueTypes[i - 1].sizeInBytes;
-    }
-  }
+      : _isAllBooleans = _valueTypes.every((t) => t == Type.boolean),
+        _fieldSetSize = (_keys.length + 7) ~/ 8,
+        _valueSizeAsBytes =
+            _valueTypes.map((t) => t._sizeInBytes).fold(0, (a, b) => a + b);
 
+  /// The schema field names and value types as a `Map`.
   Map<String, Type> toMap() => {
         for (var i = 0; i != _keys.length; ++i) _keys[i]: _valueTypes[i],
       };
@@ -37,23 +51,76 @@
   int get hashCode =>
       throw UnsupportedError('TypedMapSchema should be compared by identity.');
 
+  /// The number of fields.
   int get length => _keys.length;
 
-  int offsetAtIndex(int i) =>
-      isAllBooleans ? throw StateError('TypedMap is all bools!') : _offsets[i];
+  /// The type of the field at index [i].
+  Type _typeAtIndex(int i) => _isAllBooleans ? Type.boolean : _valueTypes[i];
 
-  Type typeAtIndex(int i) => isAllBooleans ? Type.boolean : _valueTypes[i];
+  /// The number of bytes needed to store all the values.
+  ///
+  /// If [_isAllBooleans] then this is the number of bytes needed for a bit
+  /// vector, otherwise it is the sum of [Type#_sizeInBytes] for all values.
+  int get _filledValueSize =>
+      _isAllBooleans ? _fieldSetSize : _valueSizeAsBytes;
 
-  int get fieldSetSize => (length + 7) ~/ 8;
-
-  int get valueSize => isAllBooleans ? fieldSetSize : _offsets.last;
+  /// The number of bytes needed to store the specified values.
+  ///
+  /// If [_isAllBooleans] then this is the number of bytes needed for a bit
+  /// vector of the present values, otherwise it is the sum of
+  /// [Type#_sizeInBytes] for all present values.
+  ///
+  /// Additionally returns a [bool]: whether the map is filled.
+  (int, bool) _valueSizeOf(
+      [Object? v0,
+      Object? v1,
+      Object? v2,
+      Object? v3,
+      Object? v4,
+      Object? v5,
+      Object? v6,
+      Object? v7]) {
+    if (_isAllBooleans) {
+      // All fields take up one bit, count then compute how many bytes.
+      var bits = 0;
+      if (v0 != null) ++bits;
+      if (v1 != null) ++bits;
+      if (v2 != null) ++bits;
+      if (v3 != null) ++bits;
+      if (v4 != null) ++bits;
+      if (v5 != null) ++bits;
+      if (v6 != null) ++bits;
+      if (v7 != null) ++bits;
+      return ((bits + 7) ~/ 8, bits == length);
+    } else {
+      // Sum the sizes of present values.
+      var result = 0;
+      if (v0 != null) result += _valueTypes[0]._sizeInBytes;
+      if (v1 != null) result += _valueTypes[1]._sizeInBytes;
+      if (v2 != null) result += _valueTypes[2]._sizeInBytes;
+      if (v3 != null) result += _valueTypes[3]._sizeInBytes;
+      if (v4 != null) result += _valueTypes[4]._sizeInBytes;
+      if (v5 != null) result += _valueTypes[5]._sizeInBytes;
+      if (v6 != null) result += _valueTypes[6]._sizeInBytes;
+      if (v7 != null) result += _valueTypes[7]._sizeInBytes;
+      return (result, result == _filledValueSize);
+    }
+  }
 
   @override
-  String toString() => 'TypedMapSchema$_keys$_valueTypes';
+  String toString() => 'TypedMapSchema${toMap()}';
 }
 
+/// Methods for writing and reading "typed maps".
+///
+/// A "typed map" is a `Map<String, Object?>` in a byte buffer that has a know
+/// [TypedMapSchema] specify its keys and value types. In addition, all keys
+/// and values are known at the time of writing.
+///
+/// Values are optional; that is, they can be `null`.
 extension TypedMaps on JsonBufferBuilder {
-  /// Creates and fills a "typed map".
+  /// Creates and fills a "typed map" with keys and value types specified in
+  /// [schema].
   ///
   /// It is linked to the `JsonBufferBuilder` that creates it: it can be added
   /// to any collection in the same `JsonBufferBuilder` without copying.
@@ -61,6 +128,12 @@
   ///
   /// The returned value must be separately added to the buffer. Otherwise,
   /// the buffer contains data that is not reachable from the root [map].
+  ///
+  /// TODO(davidmorgan): benchmarking suggested that specialized
+  /// implementations of this method per schema size (createTypedMap1,
+  /// createdTypedMap2, etc) are not faster in the JIT VM but they are about
+  /// 5-10% faster in the AOT VM, consider adding specialized methods. This
+  /// will presumably matter more when if larger schemas are supported.
   Map<String, Object?> createTypedMap(TypedMapSchema schema,
       [Object? v0,
       Object? v1,
@@ -70,114 +143,89 @@
       Object? v5,
       Object? v6,
       Object? v7]) {
-    explanations?.push('addTypedMap $schema');
+    _explanations?.push('addTypedMap $schema');
 
-    final filled = switch (schema.length) {
-      0 => true,
-      1 => v0 != null,
-      2 => v0 != null && v1 != null,
-      3 => v0 != null && v1 != null && v2 != null,
-      4 => v0 != null && v1 != null && v2 != null && v3 != null,
-      5 => v0 != null && v1 != null && v2 != null && v3 != null && v4 != null,
-      6 => v0 != null &&
-          v1 != null &&
-          v2 != null &&
-          v3 != null &&
-          v4 != null &&
-          v5 != null,
-      7 => v0 != null &&
-          v1 != null &&
-          v2 != null &&
-          v3 != null &&
-          v4 != null &&
-          v5 != null &&
-          v6 != null,
-      8 => v0 != null &&
-          v1 != null &&
-          v2 != null &&
-          v3 != null &&
-          v4 != null &&
-          v5 != null &&
-          v6 != null &&
-          v7 != null,
-      _ => throw UnsupportedError('Too long: ${schema.length}')
-    };
+    // Compute how much space the values need, and whether the map is filled.
+    // If the map is filled this is marked with the high bit of the schema
+    // pointer, then the field set is omitted.
+    final (valuesSize, filled) =
+        schema._valueSizeOf(v0, v1, v2, v3, v4, v5, v6, v7);
 
+    // Layout is: pointer to schema, field set (unless filled!), values.
+    final pointer = _reserve(
+        _pointerSize + (filled ? 0 : schema._fieldSetSize) + valuesSize);
+
+    // Write the pointer to schema, setting the high bit if the map is filled.
     var schemaPointer = _pointersBySchema[schema] ??=
         _addPointerTo(_addClosedMap(schema.toMap()));
     if (filled) schemaPointer |= 0x80000000;
-
-    final size = filled
-        ? schema.valueSize
-        : (v0 == null ? 0 : schema.typeAtIndex(0).sizeInBytes) +
-            (v1 == null ? 0 : schema.typeAtIndex(1).sizeInBytes) +
-            (v2 == null ? 0 : schema.typeAtIndex(2).sizeInBytes) +
-            (v3 == null ? 0 : schema.typeAtIndex(3).sizeInBytes) +
-            (v4 == null ? 0 : schema.typeAtIndex(4).sizeInBytes) +
-            (v5 == null ? 0 : schema.typeAtIndex(5).sizeInBytes) +
-            (v6 == null ? 0 : schema.typeAtIndex(6).sizeInBytes) +
-            (v7 == null ? 0 : schema.typeAtIndex(7).sizeInBytes);
-
-    final pointer =
-        _reserve(_pointerSize + (filled ? 0 : schema.fieldSetSize) + size);
     _writePointer(pointer, schemaPointer);
 
+    // If not filled, write the field set: a bit vector indicating which fields
+    // are present.
     if (!filled) {
       _setByte(
           pointer + _pointerSize,
-          (v0 == null ? 0 : 1) +
-              (v1 == null ? 0 : 2) +
-              (v2 == null ? 0 : 4) +
-              (v3 == null ? 0 : 8) +
-              (v4 == null ? 0 : 16) +
-              (v5 == null ? 0 : 32) +
-              (v6 == null ? 0 : 64) +
-              (v7 == null ? 0 : 128));
+          (v0 == null ? 0 : 0x01) +
+              (v1 == null ? 0 : 0x02) +
+              (v2 == null ? 0 : 0x04) +
+              (v3 == null ? 0 : 0x08) +
+              (v4 == null ? 0 : 0x10) +
+              (v5 == null ? 0 : 0x20) +
+              (v6 == null ? 0 : 0x40) +
+              (v7 == null ? 0 : 0x80));
     }
 
-    var offset = 0;
-
-    void addValue(int index, Object? value) {
-      explanations?.push('addValue $index $value');
-
-      if (schema.isAllBooleans) {
-        final valuePointer = pointer +
-            _pointerSize +
-            (filled ? 0 : schema.fieldSetSize) +
-            (offset ~/ 8);
-        final bitIndex = offset % 8;
-        _setBit(valuePointer, bitIndex, value as bool);
-        offset++;
-      } else {
-        final valuePointer = pointer +
-            _pointerSize +
-            (filled ? 0 : schema.fieldSetSize) +
-            offset;
-        final valueType = schema.typeAtIndex(index);
-        _writeAnyOfType(valueType, valuePointer, value);
-        offset += valueType.sizeInBytes;
+    // Write the values.
+    var valuePointer =
+        pointer + _pointerSize + (filled ? 0 : schema._fieldSetSize);
+    if (schema._isAllBooleans) {
+      // If all booleans, pack into a byte bit vector.
+      var byte = 0;
+      var bitmask = 0x01;
+      void addBit(bool bit) {
+        if (bit) byte += bitmask;
+        bitmask <<= 1;
       }
-      explanations?.pop();
+
+      if (v0 != null) addBit(v0 == true);
+      if (v1 != null) addBit(v1 == true);
+      if (v2 != null) addBit(v2 == true);
+      if (v3 != null) addBit(v3 == true);
+      if (v4 != null) addBit(v4 == true);
+      if (v5 != null) addBit(v5 == true);
+      if (v6 != null) addBit(v6 == true);
+      if (v7 != null) addBit(v7 == true);
+      _setByte(valuePointer, byte);
+    } else {
+      // If not all booleans, write only present values according to their
+      // size in bytes.
+      void addValue(int index, Object? value) {
+        _explanations?.push('addValue $index $value');
+        final valueType = schema._typeAtIndex(index);
+        _writeAnyOfType(valueType, valuePointer, value);
+        valuePointer += valueType._sizeInBytes;
+        _explanations?.pop();
+      }
+
+      if (v0 != null) addValue(0, v0);
+      if (v1 != null) addValue(1, v1);
+      if (v2 != null) addValue(2, v2);
+      if (v3 != null) addValue(3, v3);
+      if (v4 != null) addValue(4, v4);
+      if (v5 != null) addValue(5, v5);
+      if (v6 != null) addValue(6, v6);
+      if (v7 != null) addValue(7, v7);
     }
 
-    if (v0 != null) addValue(0, v0);
-    if (v1 != null) addValue(1, v1);
-    if (v2 != null) addValue(2, v2);
-    if (v3 != null) addValue(3, v3);
-    if (v4 != null) addValue(4, v4);
-    if (v5 != null) addValue(5, v5);
-    if (v6 != null) addValue(6, v6);
-    if (v7 != null) addValue(7, v7);
-
-    explanations?.pop();
+    _explanations?.pop();
     return _TypedMap(this, pointer);
   }
 
-  /// Returns the [Pointer] to [map].
+  /// Returns the [_Pointer] to [map].
   ///
-  /// The [map] must have been created in this buffer using
-  /// [createTypedMap].
-  Pointer _pointerToTypedMap(_TypedMap map) {
+  /// The [map] must have been created in this buffer using [createTypedMap].
+  _Pointer _pointerToTypedMap(_TypedMap map) {
     _checkTypedMapOwnership(map);
     return map._pointer;
   }
@@ -190,7 +238,8 @@
     }
   }
 
-  Map<String, Object?> readTypedMap(Pointer pointer) {
+  /// Returns the [_TypedMap] at [pointer].
+  Map<String, Object?> _readTypedMap(_Pointer pointer) {
     return _TypedMap(this, pointer);
   }
 }
@@ -199,21 +248,28 @@
     with MapMixin<String, Object?>, _EntryMapMixin
     implements Map<String, Object?> {
   final JsonBufferBuilder _buffer;
-  final Pointer _pointer;
+  final _Pointer _pointer;
 
   // If a `TypedMap` is created then immediately added to another `Map` then
   // these values are never needed, just the `_pointer`. Use `late` so they are
   // only computed if needed.
-  late final Pointer _schemaPointer =
-      _buffer.readPointer(_pointer) & 0x7fffffff;
+
+  late final _Pointer _schemaPointer =
+      // The high byte of the schema pointer indicates "filled", omit it.
+      _buffer._readPointer(_pointer) & 0x7fffffff;
+
+  /// The schema of this "typed map" giving its field names and types.
   late final TypedMapSchema _schema =
       _buffer._schemasByPointer[_schemaPointer] ??= TypedMapSchema(
-          _buffer.readClosedMap(_buffer.readPointer(_schemaPointer)).cast());
-  late final bool filled = (_buffer.readPointer(_pointer) & 0x80000000) != 0;
+          _buffer._readClosedMap(_buffer._readPointer(_schemaPointer)).cast());
+
+  /// Whether all fields are present, meaning no explicit field set was written.
+  late final bool filled = (_buffer._readPointer(_pointer) & 0x80000000) != 0;
 
   _TypedMap(this._buffer, this._pointer);
 
-  bool hasField(int index) {
+  /// Whether the field at [index] is present.
+  bool _hasField(int index) {
     if (index < 0 || index >= _schema.length) {
       throw RangeError.value(
           index, 'index', 'Is out of range, length: ${_schema.length}.');
@@ -221,15 +277,12 @@
     if (filled) return true;
     final byte = index ~/ 8;
     final bit = index % 8;
-    return _buffer.readBit(_pointer + _pointerSize + byte, bit);
+    return _buffer._readBit(_pointer + _pointerSize + byte, bit);
   }
 
   @override
   Object? operator [](Object? key) {
     final iterator = entries.iterator;
-    // TODO(davidmorgan): for small maps this is probably already efficient
-    // enough. Do something better for large maps, for example sorting keys
-    // and binary search?
     while (iterator.moveNext()) {
       if (iterator.current.key == key) return iterator.current.value;
     }
@@ -240,33 +293,26 @@
   @override
   late final int length = _computeLength();
 
+  /// Computes length from present fields.
   int _computeLength() {
     if (filled) return _schema.length;
     var result = 0;
     for (var i = 0; i != _schema.length; ++i) {
-      if (hasField(i)) ++result;
+      if (_hasField(i)) ++result;
     }
     return result;
   }
 
-  // For performance, `MapMixin` uses `keys.isEmpty`.
-  @override
-  bool get isEmpty => filled ? _schema.length == 0 : length == 0;
-
-// For performance, `MapMixin` uses `keys.isNotEmpty`.
-  @override
-  bool get isNotEmpty => !isEmpty;
-
   @override
   late final Iterable<String> keys = _IteratorFunctionIterable<String>(
-      _schema.isAllBooleans
+      _schema._isAllBooleans
           ? () => _AllBoolsTypedMapKeyIterator(this)
           : () => _PartialTypedMapKeyIterator(this),
       length: length);
 
   @override
   late final Iterable<Object?> values = _IteratorFunctionIterable<Object?>(
-      _schema.isAllBooleans
+      _schema._isAllBooleans
           ? () => _AllBoolsTypedMapValueIterator(this)
           : () => _PartialTypedMapValueIterator(this),
       length: length);
@@ -274,42 +320,49 @@
   @override
   late Iterable<MapEntry<String, Object?>> entries =
       _IteratorFunctionIterable<MapEntry<String, Object?>>(
-          _schema.isAllBooleans
+          _schema._isAllBooleans
               ? () => _AllBoolsTypedMapEntryIterator(this)
               : () => _PartialTypedMapEntryIterator(this),
           length: length);
 
   @override
   void operator []=(String key, Object? value) {
-    throw UnsupportedError('JsonBuffer is readonly.');
+    throw UnsupportedError(
+        'This JsonBufferBuilder map is read-only, see "createGrowableMap".');
   }
 
   @override
   Object? remove(Object? key) {
-    throw UnsupportedError('JsonBuffer is readonly.');
+    throw UnsupportedError(
+        'This JsonBufferBuilder map is read-only, see "createGrowableMap".');
   }
 
   @override
   void clear() {
-    throw UnsupportedError('JsonBuffer is readonly.');
+    throw UnsupportedError(
+        'This JsonBufferBuilder map is read-only, see "createGrowableMap".');
   }
 }
 
-/// `Iterator` that reads a `Map` in a [JsonBufferBuilder].
+/// `Iterator` that reads a "typed map" in a [JsonBufferBuilder].
 abstract class _PartialTypedMapIterator<T> implements Iterator<T> {
   final _TypedMap _map;
   final JsonBufferBuilder _buffer;
   final TypedMapSchema _schema;
-  final Pointer _valuesPointer;
-  int _offset = -1;
+  final _Pointer _valuesPointer;
+
+  /// The current field's index in the schema.
   int _index = -1;
 
+  /// The current byte offset from [_valuesPointer.]
+  int _offset = -1;
+
   _PartialTypedMapIterator(this._map)
       : _buffer = _map._buffer,
         _schema = _map._schema,
         _valuesPointer = _map._pointer +
             _pointerSize +
-            (_map.filled ? 0 : _map._schema.fieldSetSize);
+            (_map.filled ? 0 : _map._schema._fieldSetSize);
 
   @override
   T get current;
@@ -320,11 +373,20 @@
 
   @override
   bool moveNext() {
-    _offset += _index == -1 ? 1 : _schema._valueTypes[_index].sizeInBytes;
+    // Update the offset.
+    //
+    // If before the first byte, move to it.
+    //
+    // Otherwise, the iterator is at a present value: advance by its size in bytes.
+    _offset += _index == -1 ? 1 : _schema._valueTypes[_index]._sizeInBytes;
+
+    // Update the field index.
     do {
       ++_index;
+      // If index is now outside the schema, iteration is finished.
       if (_index == _schema.length) return false;
-    } while (!_map.hasField(_index));
+      // Skip missing fields.
+    } while (!_map._hasField(_index));
     return true;
   }
 }
@@ -351,14 +413,21 @@
   MapEntry<String, Object?> get current => MapEntry(_currentKey, _currentValue);
 }
 
-/// `Iterator` that reads a `Map` in a [JsonBufferBuilder].
+/// `Iterator` that reads a "typed map" in a [JsonBufferBuilder] with all
+/// values of type [Type.boolean].
 abstract class _AllBoolsTypedMapIterator<T> implements Iterator<T> {
   final _TypedMap _map;
   final JsonBufferBuilder _buffer;
   final TypedMapSchema _schema;
-  final Pointer _valuesPointer;
+  final _Pointer _valuesPointer;
+
+  /// The current field's index in the schema.
   int _index = -1;
+
+  /// The byte offset from [_valuesPointer].
   int _intOffset = -1;
+
+  /// The number of the current bit in the current byte.
   int _bitOffset = 7;
 
   _AllBoolsTypedMapIterator(this._map)
@@ -366,26 +435,31 @@
         _schema = _map._schema,
         _valuesPointer = _map._pointer +
             _pointerSize +
-            (_map.filled ? 0 : _map._schema.fieldSetSize);
+            (_map.filled ? 0 : _map._schema._fieldSetSize);
 
   @override
   T get current;
 
   String get _currentKey => _schema._keys[_index];
   Object? get _currentValue =>
-      _buffer.readBit(_valuesPointer + _intOffset, _bitOffset);
+      _buffer._readBit(_valuesPointer + _intOffset, _bitOffset);
 
   @override
   bool moveNext() {
+    // Update the offset: advance by one bit.
     ++_bitOffset;
     if (_bitOffset == 8) {
       _bitOffset = 0;
       ++_intOffset;
     }
+
+    // Update the field index.
     do {
       ++_index;
+      // If index is now outside the schema, iteration is finished.
       if (_index == _map._schema.length) return false;
-    } while (!_map.hasField(_index));
+      // Skip missing fields.
+    } while (!_map._hasField(_index));
     return true;
   }
 }
diff --git a/pkgs/dart_model/test/json_buffer/closed_map_test.dart b/pkgs/dart_model/test/json_buffer/closed_map_test.dart
index d7f10ff..faa6633 100644
--- a/pkgs/dart_model/test/json_buffer/closed_map_test.dart
+++ b/pkgs/dart_model/test/json_buffer/closed_map_test.dart
@@ -13,11 +13,11 @@
 
     setUp(() {
       builder = JsonBufferBuilder();
-      explanations = Explanations();
     });
 
     tearDown(() {
-      print(builder);
+      printOnFailure('Try: dart -Ddebug_json_buffer=true test -c source');
+      printOnFailure(builder.toString());
     });
 
     test('simple write and read', () {
diff --git a/pkgs/dart_model/test/json_buffer/growable_map_test.dart b/pkgs/dart_model/test/json_buffer/growable_map_test.dart
index 42e41f5..932e701 100644
--- a/pkgs/dart_model/test/json_buffer/growable_map_test.dart
+++ b/pkgs/dart_model/test/json_buffer/growable_map_test.dart
@@ -13,11 +13,11 @@
 
     setUp(() {
       builder = JsonBufferBuilder();
-      explanations = Explanations();
     });
 
     tearDown(() {
-      print(builder);
+      printOnFailure('Try: dart -Ddebug_json_buffer=true test -c source');
+      printOnFailure(builder.toString());
     });
 
     test('can be written and read if empty', () {
diff --git a/pkgs/dart_model/test/json_buffer/json_buffer_builder_test.dart b/pkgs/dart_model/test/json_buffer/json_buffer_builder_test.dart
index 981fb0f..78d1e52 100644
--- a/pkgs/dart_model/test/json_buffer/json_buffer_builder_test.dart
+++ b/pkgs/dart_model/test/json_buffer/json_buffer_builder_test.dart
@@ -11,11 +11,11 @@
 
     setUp(() {
       builder = JsonBufferBuilder();
-      explanations = Explanations();
     });
 
     tearDown(() {
-      print(builder);
+      printOnFailure('Try: dart -Ddebug_json_buffer=true test -c source');
+      printOnFailure(builder.toString());
     });
 
     test('writes and reads supported types', () {
diff --git a/pkgs/dart_model/test/json_buffer/typed_map_test.dart b/pkgs/dart_model/test/json_buffer/typed_map_test.dart
index d3d53f0..d65ba1f 100644
--- a/pkgs/dart_model/test/json_buffer/typed_map_test.dart
+++ b/pkgs/dart_model/test/json_buffer/typed_map_test.dart
@@ -13,11 +13,11 @@
 
     setUp(() {
       builder = JsonBufferBuilder();
-      explanations = Explanations();
     });
 
     tearDown(() {
-      print(builder);
+      printOnFailure('Try: dart -Ddebug_json_buffer=true test -c source');
+      printOnFailure(builder.toString());
     });
 
     test('with some values missing can be written and read', () {
@@ -29,7 +29,6 @@
         'missing2': Type.stringPointer
       });
       final map = builder.createTypedMap(schema, 'aa', true, null, 12345, null);
-      print(map);
       expectFullyEquivalentMaps(map, {'a': 'aa', 'b': true, 'c': 12345});
     });
 
@@ -123,6 +122,33 @@
       expect(length2 - length1, 4 + 1);
     });
 
+    test('if all fields are bools or nulls they are packed into bits', () {
+      final schema = TypedMapSchema({
+        'a': Type.boolean,
+        'b': Type.boolean,
+        'c': Type.boolean,
+        'd': Type.boolean,
+        'e': Type.boolean,
+        'f': Type.boolean,
+        'g': Type.boolean,
+        'h': Type.boolean,
+      });
+
+      // Write once so schema is written.
+      builder.createTypedMap(
+          schema, null, false, true, false, true, false, true, false);
+
+      // Check length of write with already-written schema.
+      final length1 = builder.length;
+      builder.createTypedMap(
+          schema, true, null, true, false, true, false, true, false);
+      final length2 = builder.length;
+
+      // Size should be schema pointer, one byte field set, one byte for seven
+      // bools.
+      expect(length2 - length1, 4 + 1 + 1);
+    });
+
     test('if not filled skipped fields save space', () {
       final schema = TypedMapSchema({
         'a': Type.uint32,