This is an automated email from the ASF dual-hosted git repository. chaokunyang pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/fory-site.git
commit 5d3dc860cf3830f8c49c17478c9dad8fab86ebbf Author: chaokunyang <[email protected]> AuthorDate: Thu May 14 05:21:45 2026 +0000 π synced local 'docs/guide/' with remote 'docs/guide/' --- docs/guide/java/field-configuration.md | 76 ++++++---- docs/guide/kotlin/index.md | 2 + docs/guide/kotlin/static-generated-serializers.md | 165 ++++++++++++++++++++++ docs/guide/xlang/field-nullability.md | 7 +- 4 files changed, 217 insertions(+), 33 deletions(-) diff --git a/docs/guide/java/field-configuration.md b/docs/guide/java/field-configuration.md index 7992f4afc5..1d119c1fc0 100644 --- a/docs/guide/java/field-configuration.md +++ b/docs/guide/java/field-configuration.md @@ -25,7 +25,8 @@ This page explains how to configure field-level metadata for serialization in Ja Apache Foryβ’ provides field-level configuration through annotations: -- **`@ForyField`**: Configure field metadata (id, nullable, ref, dynamic) +- **`@ForyField`**: Configure field metadata (id, ref, dynamic) +- **`@Nullable`**: Mark a field type or nested type position as nullable - **`@Ignore`**: Exclude fields from serialization - **Integer type annotations**: Control integer encoding (varint, fixed, tagged, unsigned) @@ -44,6 +45,7 @@ Use annotations on fields: ```java import org.apache.fory.annotation.ForyField; +import org.apache.fory.annotation.Nullable; public class Person { @ForyField(id = 0) @@ -52,7 +54,8 @@ public class Person { @ForyField(id = 1) private int age; - @ForyField(id = 2, nullable = true) + @Nullable + @ForyField(id = 2) private String nickname; } ``` @@ -69,7 +72,8 @@ public class User { @ForyField(id = 1) private String name; - @ForyField(id = 2, nullable = true) + @Nullable + @ForyField(id = 2) private String email; @ForyField(id = 3, ref = true) @@ -82,12 +86,14 @@ public class User { ### Parameters -| Parameter | Type | Default | Description | -| ---------- | --------- | ------- | -------------------------------------- | -| `id` | `int` | `-1` | Non-negative field tag ID, or no ID | -| `nullable` | `boolean` | `false` | Whether the field can be null | -| `ref` | `boolean` | `false` | Enable reference tracking | -| `dynamic` | `Dynamic` | `AUTO` | Control polymorphism for struct fields | +| Parameter | Type | Default | Description | +| --------- | --------- | ------- | -------------------------------------- | +| `id` | `int` | `-1` | Non-negative field tag ID, or no ID | +| `ref` | `boolean` | `false` | Enable reference tracking | +| `dynamic` | `Dynamic` | `AUTO` | Control polymorphism for struct fields | + +Use `@Nullable` on the field type or nested type position for nullable schema +metadata. `@ForyField` does not carry nullability. ## Field ID (`id`) @@ -130,18 +136,20 @@ public class User { } ``` -## Nullable Fields (`nullable`) +## Nullable Fields (`@Nullable`) -Use `nullable = true` for fields that can be `null`: +Use `@Nullable` for fields that can be `null`: ```java public class Record { // Nullable string field - @ForyField(id = 0, nullable = true) + @Nullable + @ForyField(id = 0) private String optionalName; // Nullable Integer field (boxed type) - @ForyField(id = 1, nullable = true) + @Nullable + @ForyField(id = 1) private Integer optionalCount; // Non-nullable field (default) @@ -152,9 +160,9 @@ public class Record { **Notes**: -- Default is `nullable = false` (non-nullable) -- When `nullable = false`, Fory skips writing the null flag (saves 1 byte) -- Boxed types (`Integer`, `Long`, etc.) that can be null should use `nullable = true` +- Xlang fields are non-nullable by default. +- When a field is non-nullable, Fory skips writing the null flag. +- Boxed types (`Integer`, `Long`, etc.) that can be null should use `@Nullable`. ## Reference Tracking (`ref`) @@ -163,10 +171,12 @@ Enable reference tracking for fields that may be shared or circular: ```java public class RefOuter { // Both fields may point to the same inner object - @ForyField(id = 0, ref = true, nullable = true) + @Nullable + @ForyField(id = 0, ref = true) private RefInner inner1; - @ForyField(id = 1, ref = true, nullable = true) + @Nullable + @ForyField(id = 1, ref = true) private RefInner inner2; } @@ -175,7 +185,8 @@ public class CircularRef { private String name; // Self-referencing field for circular references - @ForyField(id = 1, ref = true, nullable = true) + @Nullable + @ForyField(id = 1, ref = true) private CircularRef selfRef; } ``` @@ -397,6 +408,7 @@ import org.apache.fory.Fory; import org.apache.fory.annotation.ForyField; import org.apache.fory.annotation.Ignore; import org.apache.fory.annotation.Int64Type; +import org.apache.fory.annotation.Nullable; import org.apache.fory.annotation.UInt64Type; import org.apache.fory.config.Int64Encoding; @@ -413,7 +425,8 @@ public class Document { private int version; // Nullable field - @ForyField(id = 2, nullable = true) + @Nullable + @ForyField(id = 2) private String description; // Collection fields @@ -437,7 +450,8 @@ public class Document { private @UInt64Type(encoding = Int64Encoding.TAGGED) long checksum; // tagged encoding // Reference-tracked field for shared/circular references - @ForyField(id = 9, ref = true, nullable = true) + @Nullable + @ForyField(id = 9, ref = true) private Document parent; // Ignored field (not serialized) @@ -487,7 +501,8 @@ public class CrossLangData { @ForyField(id = 2) private @UInt64Type(encoding = Int64Encoding.TAGGED) long longTagged; - @ForyField(id = 3, nullable = true) + @Nullable + @ForyField(id = 3) private String optionalValue; } ``` @@ -514,7 +529,8 @@ public class DataV2 { @ForyField(id = 1) private String name; - @ForyField(id = 2, nullable = true) + @Nullable + @ForyField(id = 2) private String email; // New field } ``` @@ -561,13 +577,13 @@ public class User { Xlang mode has **stricter default values** due to type system differences between languages: -- **Nullable**: Fields are non-nullable by default (`nullable = false`) +- **Nullable**: Fields are non-nullable by default - **Ref tracking**: Disabled by default (`ref = false`) - **Polymorphism**: Concrete types are non-polymorphic by default In xlang mode, you **need to configure fields** when: -- A field can be null (use `nullable = true`) +- A field can be null (use `@Nullable`) - A field needs reference tracking for shared/circular objects (use `ref = true`) - Integer types need specific encoding for cross-language compatibility - You want to reduce metadata size (use field IDs) @@ -581,10 +597,12 @@ public class User { @ForyField(id = 1) private String name; - @ForyField(id = 2, nullable = true) // Must declare nullable + @Nullable + @ForyField(id = 2) // Must declare @Nullable private String email; - @ForyField(id = 3, ref = true, nullable = true) // Must declare ref for shared objects + @Nullable + @ForyField(id = 3, ref = true) // Must declare ref for shared objects private User friend; } ``` @@ -600,7 +618,7 @@ public class User { ## Best Practices 1. **Configure field IDs**: Recommended for compatible mode to reduce serialization cost -2. **Use `nullable = true` for nullable fields**: Required for fields that can be null +2. **Use `@Nullable` for nullable fields**: Required for fields that can be null 3. **Enable ref tracking for shared objects**: Use `ref = true` when objects are shared or circular 4. **Use `@Ignore` or `transient` for sensitive data**: Passwords, tokens, internal state 5. **Choose appropriate encoding**: `varint` for small values, `fixed` for full-range values @@ -612,7 +630,7 @@ public class User { | Annotation | Description | | ----------------------------- | -------------------------------------- | | `@ForyField(id = N)` | Field tag ID to reduce metadata size | -| `@ForyField(nullable = true)` | Mark field as nullable | +| `@Nullable` | Mark field or nested type as nullable | | `@ForyField(ref = true)` | Enable reference tracking | | `@ForyField(dynamic = ...)` | Control polymorphism for struct fields | | `@Ignore` | Exclude field from serialization | diff --git a/docs/guide/kotlin/index.md b/docs/guide/kotlin/index.md index 1fcee0430b..af8f6bf106 100644 --- a/docs/guide/kotlin/index.md +++ b/docs/guide/kotlin/index.md @@ -38,6 +38,7 @@ Fory Kotlin inherits all features from Fory Java, plus Kotlin-specific optimizat - **High Performance**: JIT code generation, zero-copy, 20-170x faster than traditional serialization - **Kotlin Type Support**: Optimized serializers for data classes, unsigned types, ranges, and stdlib types - **Default Value Support**: Automatic handling of Kotlin data class default parameters during schema evolution +- **Static Xlang Serializers**: KSP-generated schema serializers for Kotlin/JVM and Android xlang mode - **Schema Evolution**: Forward/backward compatibility for class schema changes See [Java Features](../java/index.md#features) for complete feature list. @@ -106,3 +107,4 @@ Fory Kotlin is built on top of Fory Java. Most configuration options, features, - [Fory Creation](fory-creation.md) - Kotlin-specific Fory setup requirements - [Type Serialization](type-serialization.md) - Serializing Kotlin types - [Default Values](default-values.md) - Kotlin data class default values support +- [Static Generated Serializers](static-generated-serializers.md) - KSP xlang/schema serializer generation diff --git a/docs/guide/kotlin/static-generated-serializers.md b/docs/guide/kotlin/static-generated-serializers.md new file mode 100644 index 0000000000..f2a2e48b1b --- /dev/null +++ b/docs/guide/kotlin/static-generated-serializers.md @@ -0,0 +1,165 @@ +--- +title: Static Generated Serializers +sidebar_position: 4 +id: static_generated_serializers +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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--- + +`fory-kotlin-ksp` generates Kotlin source serializers for Fory xlang/schema +mode. The generated serializers use the existing Java runtime +`WriteContext`, `ReadContext`, and `MemoryBuffer`; they do not define a +Kotlin-only protocol or buffer abstraction. + +## Scope + +KSP generation is only for xlang/schema mode. It does not generate Fory Java +native object serializers and does not preserve concrete JVM runtime identity +for object graphs. Collection declarations are schema carriers: `List<T>` is +encoded as `list<T>`, `Map<K, V>` as `map<K, V>`, and deserialization only +guarantees a value assignable to the declared Kotlin field type. + +Mutable collection interface fields are supported by reconstructing a mutable +implementation assignable to the declared type. Sorted collection declarations +without an explicit comparator, such as `TreeSet` and `ConcurrentSkipListSet`, +are accepted only for non-null scalar or string elements. Concurrent map +declarations are accepted only with non-null values because the JVM concurrent +map implementations reject null entries. + +Kotlin/JVM and Android are supported. Kotlin/Native and Kotlin/JS are not +supported by this module. + +Phase 1 KSP generation emits serializers for public concrete, non-generic +`@ForyStruct` classes with primary-constructor fields and a public or internal +primary constructor. Kotlin `private` and `internal` struct classes are rejected +because generated serializers and SPI providers are public runtime entry +points. Kotlin `object` declarations, annotated interfaces, abstract classes, +sealed declarations, and generic struct targets are rejected during KSP +processing because they are not single constructor-field serializer targets for +the phase 1 generator. Dense primitive and unsigned array types, such as +`IntArray` and `UIntArray`, are supported as top-level fields. Nested dense +arrays, such as `List<UIntArray>` or `Map<String, IntArray>`, are rejected in +phase 1. Fory Java `@ArrayType` is also supported on top-level `List<T>` fields +when `T` is a non-null bool or numeric dense-array element domain. The generated +serializer writes the field as dense `array<T>` schema and converts decoded JVM +list elements back to the declared Kotlin carrier, such as `Int`, `UInt`, or +`Double`. + +## Annotations + +Reuse Java annotations for Fory concepts: + +```kotlin +import org.apache.fory.annotation.ForyField +import org.apache.fory.annotation.ForyStruct +import org.apache.fory.kotlin.Fixed +import org.apache.fory.kotlin.VarInt + +@ForyStruct +data class User( + @ForyField(id = 1) + val id: @Fixed UInt, + + @ForyField(id = 2) + val score: @VarInt Long, +) +``` + +Use `@ForyField(id = 1)`. `@field:ForyField(id = 1)` is also accepted for +field-backed properties. `@get:ForyField` and `@set:ForyField` are rejected +because accessors are not schema fields. + +Kotlin nullability is expressed with `?`, including nested positions such as +`List<String?>?`. Do not use Fory `@Nullable` in Kotlin source. + +`@ForyField(ref = true)` is rejected by the KSP xlang processor. Phase 1 +generated serializers construct Kotlin values through primary constructors and +therefore cannot publish partially constructed objects for cyclic +back-references. Use non-cyclic xlang schemas for Kotlin KSP serializers. + +Kotlin default constructor arguments are supported for compatible reads by +generating constructor calls that omit missing defaulted arguments. Because +Kotlin source generation must emit these calls statically, the first KSP +implementation supports at most 12 defaulted constructor fields per struct. + +## Encoding + +Kotlin type-use encoding annotations map to Fory xlang integer encodings: + +| Annotation | Valid Kotlin types | +| ---------- | ------------------------------ | +| `@Fixed` | `Int`, `Long`, `UInt`, `ULong` | +| `@VarInt` | `Int`, `Long`, `UInt`, `ULong` | +| `@Tagged` | `Long`, `ULong` | + +Without an annotation, xlang `Int`, `Long`, `UInt`, and `ULong` use varint +encoding. This is required by xlang mode and is not controlled by Java native +mode numeric compression options. + +## Registration + +Users register Kotlin struct classes with the normal Fory Java registration +APIs. Do not reference generated serializer class names. + +```kotlin +val fory = Fory.builder() + .withXlang(true) + .requireClassRegistration(true) + .build() + +KotlinSerializers.registerSerializers(fory) +fory.register(User::class.java, "example", "User") +``` + +The KSP processor emits a service provider that maps target classes to their +generated serializers. On Android and for Kotlin classes, the Java runtime +requires this provider mapping for xlang structs. Missing KSP/SPI metadata is a +configuration error, not a reflective fallback path. This avoids generated-name +`Class.forName` lookups and works as an R8-compatible discovery path when the +generated service resource and serializer classes are kept by the application +build. + +Android builds that run R8 must preserve the generated SPI resource and the +generated provider/serializer classes. Application shrinker rules should keep: + +```text +-keep class * implements org.apache.fory.resolver.StaticGeneratedSerializerProvider { *; } +-keep class * implements org.apache.fory.resolver.StaticGeneratedSerializerProvider$KotlinSymbolProcessor { *; } +-keep class * implements org.apache.fory.resolver.StaticGeneratedSerializerProvider$JavaAnnotationProcessor { *; } +-keep class **.*__ForySerializer__ { *; } +-keep class org.apache.fory.resolver.StaticGeneratedSerializerProvider { *; } +-keep class org.apache.fory.resolver.StaticGeneratedSerializerProvider$* { *; } +``` + +The Kotlin KSP generated service resource +`META-INF/services/org.apache.fory.resolver.StaticGeneratedSerializerProvider$KotlinSymbolProcessor` +must also be packaged. Java annotation-processor generated serializers use the +separate +`META-INF/services/org.apache.fory.resolver.StaticGeneratedSerializerProvider$JavaAnnotationProcessor` +resource so mixed Java/Kotlin artifacts do not overwrite one another's provider +lists. If a build plugin strips service resources, configure the application +packaging step to retain these files. + +The KSP processor emits one aggregate provider for the compilation module in +`org.apache.fory.kotlin.generated`. The provider class name includes a stable +hash of the generated target mappings, so multiple Kotlin artifacts can publish +providers on the same classpath without binary-name collisions. That provider +contains mappings for all annotated Kotlin structs in the module, across source +packages. + +Applications must package the generated service resource. There is no manual +provider registration API; users still register only target classes and their +IDs or xlang names through the normal Fory Java registration APIs. diff --git a/docs/guide/xlang/field-nullability.md b/docs/guide/xlang/field-nullability.md index 5b9d645768..bea7f3f092 100644 --- a/docs/guide/xlang/field-nullability.md +++ b/docs/guide/xlang/field-nullability.md @@ -99,7 +99,7 @@ public class Person { List<String> tags; // Must not be null // Explicitly nullable - @ForyField(nullable = true) + @Nullable String nickname; // Can be null // Optional wrapper - nullable by default @@ -188,14 +188,13 @@ FORY_STRUCT(Person, name, age, tags, nickname, bio); ## Customizing Nullability -### Java: @ForyField Annotation +### Java: @Nullable Annotation ```java public class Config { - @ForyField(nullable = true) + @Nullable String optionalSetting; // Explicitly nullable - @ForyField(nullable = false) String requiredSetting; // Explicitly non-nullable (default) } ``` --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
