title: Schema Evolution sidebar_position: 10 id: schema_evolution license: | Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Schema evolution lets different versions of your app exchange messages safely — a v2 writer can produce a message that a v1 reader can still decode, and vice versa.
Compatible mode is the Dart default. Keep this default when services may run different versions at the same time, for example during a rolling deployment or when clients are not updated immediately.
final fory = Fory();
In compatible mode, Fory includes enough field metadata in each message so that the reader can skip unknown fields and use defaults for missing ones. Use stable field IDs (see below) to anchor the schema across changes.
Compatible readers also tolerate selected scalar field type changes when the value is lossless. A matched field can read between bool, String, numeric scalars, and Decimal when the converted value has the same logical value. For example, "true" and "false" can be read as booleans, "123" can be read as a numeric field that can hold 123, numbers and decimals can be read as canonical strings, and numeric widening or narrowing succeeds only when no precision or range is lost. Scalar conversion applies only to matched compatible fields, not root values or collection elements. String-to-number conversion accepts finite ASCII decimal literals without whitespace, a leading +, Unicode digits, underscores, NaN, or Infinity. Nullable fields still compose with these conversions, but reference-tracked scalar type changes are incompatible. Invalid strings, out-of-range values, and lossy conversions fail with InvalidDataException during deserialization.
To use compatible mode safely, mark your structs with @ForyStruct(evolving: true) (the default) and assign a stable @ForyField(id: ...) to every field before you ship your first payload:
@ForyStruct(evolving: true) class UserProfile { UserProfile(); @ForyField(id: 1) String name = ''; @ForyField(id: 2, nullable: true) String? nickname; }
If you add field IDs after payloads are already in production, existing stored messages won‘t have them and evolution won’t work correctly.
For an ordinary inherited struct, assign IDs across every field included in the concrete child's flattened schema. An ID used by an included parent or mixin field cannot be reused by the child.
For an external structural serializer, the local serializer declaration supplies the evolving schema and each declaration field must match the corresponding target property.
Safe changes (compatible on both sides):
@ForyField(id: ...) stays the same.Unsafe changes (may break existing messages):
id or name) of a type after messages are in production.Evolution only works when all peers that exchange messages agree on:
compatible setting.name).Test rolling-upgrade scenarios with real round trips before deploying.
Use compatible: false only when every reader and writer always uses the same schema and you want faster serialization and smaller size. For xlang payloads, set compatible: false only after verifying that every language uses the same schema, or when native types are generated from Fory schema IDL.
final fory = Fory(compatible: false);
Regenerate every affected .fory.dart part after adding inherited storage, changing a superclass or mixin, changing exposePrivateFields, or changing ignoreInheritedPrivateFields. Generate a dependency package's provider part before building consumers that include its private fields.
Enabling ignoreInheritedPrivateFields removes every private ancestor and applied-mixin field from that concrete child's generated schema. Disabling it adds those fields back and may require provider companions. Compatible schemas use their normal missing-field and unknown-field behavior. Fixed-schema peers must change together because the resulting field list is different. A parent annotation does not propagate this setting to children.
This option changes generated field selection only. It does not change the runtime reference protocol or add a compatibility reader.