Add annotation provider#1
Conversation
Added `TypeAnnotationProvider` and `TypeAnnotationOptions` for json metadata.
|
Originally intended to open a second pull request for |
|
Here's how the annotations currently work for my types. const _CardDescriptor: json.TypeAnnotationProvider(card_descriptor.CardDescriptor) = .{
.json_rename = &.{
.{ .to = "se", .from = "special_abilities" },
.{ .to = "t", .from = "tags" },
},
.json_skip = &[_][]const u8{ "_isGravestone", "_isSurprise" },
};
const _EffectEntityComponent: json.TypeAnnotationProvider(EffectEntityComponent) = .{
.json_tag = "$type",
.json_payload = "$data",
};
const _Query: json.TypeAnnotationProvider(Query) = .{
.json_tag = "$type",
.json_payload = "$data",
.json_rename = &.{
.{
.from = "PvZCards.Engine.Queries.AlwaysMatchesQuery, EngineLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
.to = "PvZCards.Engine.Queries.TestRename, EngineLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
},
},
};
const _Component: json.TypeAnnotationProvider(Component) = .{
.json_tag = "$type",
.json_payload = "$data",
};
pub const ComponentUnionRegistry = json.TypeAnnotationOptions(.{ _Component, _EffectEntityComponent, _Query, _CardDescriptor }); |
sakakibara
left a comment
There was a problem hiding this comment.
Thanks for putting this together. This is a useful mechanism. Supporting annotations for external and comptime-generated types fills a real gap, and rejecting invalid entries at comptime is the right approach.
There are a few API and correctness details to address before merging. I left inline comments on source compatibility, annotation precedence, and void json_payload encoding.
Please also:
- Add provider-path tests for
json_rename,json_skip,json_flatten,json_tag,json_payload,fromJson, andtoJson. - Cover provider precedence and sorted and unsorted encode/decode round trips.
- Add an
Unreleasedentry toCHANGELOG.mdfor the new public API. - Remove the extra space in the README snippet:
cfg, annotations, arena.
| /// in use. | ||
| pub fn parseInto(comptime T: type, arena: std.mem.Allocator, src: []const u8, options: ParseOptions) (Error || DecodeError)!T { | ||
| return decode_mod.parseInto(T, arena, src, options); | ||
| pub fn parseInto(comptime T: type, comptime TAnnotation: type, arena: std.mem.Allocator, src: []const u8, options: ParseOptions) (Error || DecodeError)!T { |
There was a problem hiding this comment.
The annotation registry should not be a new positional parameter. This makes the public typed-codec API source-incompatible even though the feature is opt-in.
Please put it in the existing options structs as a defaulted field, for example annotations: type = DefaultTypes. Existing calls such as parseInto(T, arena, src, .{}) would remain valid, while callers could opt in with .{ .annotations = MyAnnotations }. The lowercase field name also follows the style of dialect and ignore_unknown_fields.
| } | ||
|
|
||
| /// Returns true if `field_name` on type `T` is listed in `T.json_skip` | ||
| /// or `TAnnotation.json_skip`. |
There was a problem hiding this comment.
The override behavior is inconsistent here. When a provider exists, json_skip = null suppresses the type-local T.json_skip; json_flatten behaves the same way. In contrast, json_rename = null, or a provider rename list without this field, falls back to T.json_rename.
Please define one precedence rule and apply it consistently across every annotation. My recommendation is that each non-null provider property overrides its type-local counterpart, while null falls back to the type-local declaration. Please cover both override and fallback behavior in tests.
| try writeQuotedString(w, comptime decode_mod.renamedKey(T, TAnnotation, union_field.name)); | ||
|
|
||
| if (decode_mod.payloadField(T, TAnnotation)) |payload| { | ||
| try writeTypedMember(w, payload, options, depth + 1, &first); |
There was a problem hiding this comment.
This emits invalid JSON for a void variant: writeTypedMember writes the payload key and colon, but no value is written when union_field.type == void. The sorted path also attempts to render the void value.
Please define the intended representation for a void payload, handle it consistently in sorted and unsorted encoding, and add a round-trip regression test.
|
Alright, thank you for feedback! I will look into this shortly. |
|
Ah, right, I guess I should have mentioned it in the original pull request. The reason why I opted for a function argument instead of an options struct field is because annotations have to be resolved comptime, which breaks compatibility with existing options. |
|
You're right about the comptime constraint. I verified it with a runtime-valued options call: adding a Please preserve the existing typed-codec entry points and expose custom annotations through a configured codec namespace instead: const Codec = json.TypedCodec(.{ query_annotations, component_annotations });
const value = try Codec.parseInto(T, arena, src, .{});
try Codec.encode(&writer, value, arena, .{});
I also ran the full suite and completed a broader audit. Please treat this as a correction and addendum to my original review. API naming
Correctness
Tests and documentation
I will re-review the revised API and run the full suite again once the branch is updated. |
Added
TypeAnnotationProviderandTypeAnnotationOptionsfor additional json metadata not linked toT.Reasoning:
@Structand@Union, where embedding annotations by declaring fields is currently impossible / undesirable behavior.Usage example: