ZE-006 - Annotation-Based Parsing System
Annotation-Based Parsing System
This proposal is still a draft and is subject to change. Please do not cite or reference it as a finalized design.
Features described here may not be implemented as described and cannot be used right now.
Introduction
This proposal introduces an annotation-based parsing system for Zirric, enabling flexible and format-agnostic encoding/decoding of data types. The system leverages Zirric’s annotation capabilities to define how data should be serialized and deserialized, supporting multiple formats (e.g., JSON, YAML, Protobuf) through modular extensions.
Motivation
Zirric currently lacks a unified mechanism for encoding/decoding data types to/from various formats. This proposal aims to:
- Provide a format-agnostic core system for encoding/decoding.
- Support format-specific extensions (e.g., JSON, YAML, Protobuf).
- Use annotations to define serialization rules, enabling fine-grained control over encoding/decoding behavior.
- Allow mixins for reusable annotation groups, reducing boilerplate.
Proposed Solution
1. Core Modules and Types
prelude Module
Core annotations and types used across the codebase.
module prelude
extern type Binary {
@Int length
}
data None
annotation Optional
annotation Default {
@Any value
}
annotation ItemType
@AnyType type
reflect Module
Core reflection capabilities.
module reflect
data Type
data Field
@String name
@Type type
@Array(Annotation) annotations
data Annotation
@Type annotationType
@Array(Any) args
annotation Name
@String name
@Returns(Type) func typeOf(@Any value)
@Returns(Array(Field)) func fieldsOf(@Type type)
@Returns(Annotation?) func annotation(@Field field, @Type annotationType)
@Returns(Bool) func hasAnnotation(@Field field, @Type annotationType)
coding Module
Central module for encoding/decoding logic.
module coding
enum Result
Ok(@Any value)
Error(@Error error)
data Error
@String message
mixin Codable
@Encodable(encode)
@Decodable(decode)
annotation Encodable
@Func(@Any) @Returns(Result) encode
annotation Decodable
@Func(@Type @Any) @Returns(Result) decode
annotation Key
@String name
@Returns(Result) func encode(@Any value)
@Returns(Result) func decode(@Type type, @Any data)
coding.json Module
JSON-specific annotations and functions.
module coding.json
annotation Inline
annotation RawType
@AnyType type
annotation Key
@String name
annotation Decode
@Func(@Any) @Returns(Any) decode
annotation Encode
@Func(@Any) @Returns(Any) encode
annotation Type
@AnyType type
@Returns(Result) func encode(@Any value)
@Returns(Result) func decode(@Type type, @Any data)
coding.yaml Module
YAML-specific annotations and functions.
module coding.yaml
annotation RawType
@AnyType type
annotation Key
@String name
annotation Decode
@Func(@Any) @Returns(Any) decode
annotation Encode
@Func(@Any) @Returns(Any) encode
@Returns(Result) func encode(@Any value)
@Returns(Result) func decode(@Type type, @Any data)
coding.proto Module
Protobuf-specific annotations and functions.
module coding.proto
annotation RawType
@AnyType type
annotation Decode
@Func(@Any) @Returns(Any) decode
annotation Encode
@Func(@Any) @Returns(Any) encode
@Returns(Result) func encode(@Any value)
@Returns(Result) func decode(@Type type, @Binary data)
Detailed Design
1. Core Annotations and Mixins
coding.Codable: A mixin combiningcoding.Encodableandcoding.Decodable.coding.Encodableandcoding.Decodable: Annotations to mark types/fields as encodable/decodable, specifying the encoding/decoding functions.coding.Key: Specifies the key to use for encoding/decoding a field.
2. Format-Specific Annotations
json.Inline: Explicitly inlines (flattens) the fields of adatatype orenumcase during JSON encoding/decoding.json.RawType: Overrides the raw type for JSON encoding/decoding.json.Decodeandjson.Encode: Specifies custom decoding/encoding functions for a field.json.Type: Specifies the JSON type to use for encoding/decoding a specific enum case.
3. Encoding/Decoding Functions
coding.encodeandcoding.decode: Generic functions for encoding/decoding values.- Format-specific functions:
json.encode,json.decode,yaml.encode, etc.
Changes to the Standard Library
- New Modules:
coding,coding.json,coding.yaml,coding.proto. - New Types:
coding.Result,coding.Error,prelude.Binary,prelude.None. - New Annotations:
coding.Encodable,coding.Decodable,coding.Key,json.Inline, etc.
Behavior and Rules
1. Inlining
- Explicit Inlining:
@json.Inlinemust be explicitly applied to enable inlining. Nested inlining also requires explicit@json.Inlineannotations.
2. Conflict Resolution
- Parsing Errors on Conflicts: If multiple inlined fields have the same name, the parser will raise an error.
3. Optional Fields
@prelude.Optional: When used with an enum, theNonecase should be parsed due to the@json.Type(json.Null)annotation.
Examples
1. Basic Usage with Codable Mixin
module example
import coding
import coding.json
import reflect
mixin coding.Codable
data Person
@String name
@Int age
func main
let person = Person("Alice" 30)
let jsonResult = coding.json.encode(person)
2. Custom Encoding/Decoding with @json.Inline
module example
import coding
import coding.json
import reflect
@json.Inline()
enum Optional
@json.Type(json.Null)
data None
@json.Inline
data Some
@Any value
data Person
@String name
@Optional nickname
func main
let personWithNickname = Person("Alice" Optional.Some("Bob"))
let personWithoutNickname = Person("Alice" Optional.None)
let jsonWithNickname = coding.json.encode(personWithNickname)
// Output: {"name": "Alice", "value": "Bob"}
let jsonWithoutNickname = coding.json.encode(personWithoutNickname)
// Output: {"name": "Alice", "nickname": null}
3. Custom Date Encoding/Decoding
module example
import coding
import coding.json
import reflect
@Returns(Date)
func dateFromISODateString(@String raw) {
// Implementation
}
@Returns(String)
func dateToISODateString(@Date value) {
// Implementation
}
data PersonWithDate
@String name
@Date
@json.RawType(String)
@json.Decode(dateFromISODateString)
@json.Encode(dateToISODateString)
birthday
func main
let person = PersonWithDate("Alice" dateFromISODateString("2023-01-01"))
let jsonResult = coding.json.encode(person)
// Output: {"name": "Alice", "birthday": "2023-01-01"}
4. Conflict Example
module example
import coding
import coding.json
import reflect
data Child1
@String field
data Child2
@String field
data Parent
@json.Inline
@Child1 child1
@json.Inline
@Child2 child2
func main
let parent = Parent(Child1("value1") Child2("value2"))
let jsonResult = coding.json.encode(parent)
// Error: Conflicting field names "field" in inlined types Child1 and Child2
5. Optional Enum Example
module example
import coding
import coding.json
import reflect
@json.Inline()
enum Optional
@json.Type(json.Null)
data None
@json.Inline
data Some
@Any value
data Person
@String name
@Optional nickname
func main
let personWithoutNickname = Person("Alice" Optional.None)
let jsonWithoutNickname = coding.json.encode(personWithoutNickname)
// Output: {"name": "Alice", "nickname": null}
Open Questions (Resolved)
- Inlining must be explicit: Yes,
@json.Inlinemust be explicitly applied to enable inlining. - Parsing errors on conflicts: Yes, the parser will raise an error if multiple inlined fields have the same name.
- Optional Fields:
@prelude.Optionalfields with theNonecase should be parsed asnulldue to the@json.Type(json.Null)annotation.
Acknowledgements
- Inspiration: Swift’s
Codableprotocol, Go’s format-specific parsing. - Contributors: Valentin Knabel.