From 094f5ab51c34eaa373fadf712fc6e7c9101443cf Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Sun, 25 Oct 2026 01:10:21 +0200 Subject: [PATCH] docs: document reference escaping, writing and version conversion - references: local references are JSON pointers in a URI, so "/" and "~" in a key are escaped as "~1" and "~0", and the pointer is percent-decoded; add how references are written, including inlining references whose target the written document does not have. - version conversion: list which references stay references and which are written inline, how V2 channel references map to V3 channel keys, and the operations and messages a downgrade drops. - README: drop the JsonPointer.Net attribution, which is no longer a dependency, and fix the ReferenceResolutionSetting name and a server reference example. --- README.md | 5 ++--- website/docs/guides/references.mdx | 24 ++++++++++++++++++++++ website/docs/guides/version-conversion.mdx | 15 +++++++++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f7460c1..3b7d0cc 100644 --- a/README.md +++ b/README.md @@ -129,13 +129,13 @@ All readers will write warnings and errors to the diagnostics. ## Reference Resolution -Internal references are resolved by default. This includes component and non-component references e.g `#/components/messages/MyMessage` and `#/servers/0`. +Internal references are resolved by default. This includes component and non-component references e.g `#/components/messages/MyMessage` and `#/servers/production`. External references can be resolved by setting `ReferenceResolution` to `ResolveAllReferences`. The default implementation will resolve anything prefixed with `file://`, `http://` & `https://`, however a custom implementation can be made, by inhereting from the `IStreamLoader` interface and setting the `ExternalReferenceLoader` in the `AsyncApiReaderSettings`. External references are always force converted to Json during resolution. This means that both yaml and json is supported - but not other serialization languages. ```csharp -var settings = new AsyncApiReaderSettings { ReferenceResolution = ReferenceResolution.ResolveAllReferences }; +var settings = new AsyncApiReaderSettings { ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences }; var document = new AsyncApiStringReader(settings).Read(json, out var diagnostics); ``` @@ -155,7 +155,6 @@ var asyncApiDocument = new AsyncApiStringReader(settings).Read(yaml, out var dia * [OpenAPI.Net](https://github.com/microsoft/OpenAPI.NET) - [MIT License](https://github.com/microsoft/OpenAPI.NET/blob/vnext/LICENSE) * [YamlDotNet](https://github.com/aaubry/YamlDotNet) - [MIT License](https://github.com/aaubry/YamlDotNet/blob/master/LICENSE.txt) -* [JsonPointer.Net](https://github.com/json-everything/json-everything) - [MIT License](https://github.com/json-everything/json-everything/blob/master/LICENSE) ## Contribution diff --git a/website/docs/guides/references.mdx b/website/docs/guides/references.mdx index d673f78..69b91b2 100644 --- a/website/docs/guides/references.mdx +++ b/website/docs/guides/references.mdx @@ -61,6 +61,10 @@ The V2 server fields are `url` and `protocol`; the host is not serialized as `ht Internal references are resolved by default during document reading. This covers component references such as `#/components/servers/shared` and document locations outside components, such as `#/servers/production`. The reference object remains a typed proxy: its target properties can be read directly, while `Reference.Reference` retains the URI and `UnresolvedReference` reports whether the target was found. +A local reference is a JSON pointer ([RFC 6901](https://www.rfc-editor.org/rfc/rfc6901)) into the document, so it can point at any location, not only at components: for example `#/channels/userSignup/messages/signedUp` or `#/servers/production/variables/port`. A key that contains `/` or `~` must be escaped as `~1` or `~0`. A channel keyed `user/signedup` is referenced as `#/channels/user~1signedup`; the unescaped `#/channels/user/signedup` points at a `signedup` member inside a `user` channel instead, and does not resolve. + +Because a reference is a URI, its pointer is also percent-decoded before it is evaluated. `#/servers/a%20b` and `#/servers/a b` both refer to a server keyed `a b`, and resolve to the same server. A key that contains `%` itself must encode it as `%25`: a server keyed `a%20b` is referenced as `#/servers/a%2520b`. References the library writes for such keys, for example when converting between versions, are percent-encoded the same way. + Reference resolution is part of reading a full document with `Read`; `ReadFragment` parses a fragment but does not run the document reference-resolution pass. A missing or invalid internal target is reported in diagnostics and remains unresolved. ```csharp @@ -96,6 +100,26 @@ if (production is AsyncApiServerReference serverReference) Consumers can often read a resolved proxy as the referenced type without first copying or replacing it. Keep the reference-type and resolution checks when your code requires a concrete target. For an unresolved reference, proxy getters can return `null`; avoid relying on target properties until `UnresolvedReference` is false. Reader errors and warnings are returned in `diagnostics`. +## Write references + +References are written as references, including ones whose target could not be resolved, so they are not lost. To write local references as their target instead, set `ReferenceInline` on the writer settings. External references are always written as references. With `InlineReferences`, a local reference that cannot be resolved throws an `AsyncApiWriterException`. + +```csharp +using System.IO; +using ByteBard.AsyncAPI.Models; +using ByteBard.AsyncAPI.Writers; + +var settings = new AsyncApiWriterSettings +{ + ReferenceInline = ReferenceInlineSetting.InlineReferences, +}; + +using var stream = new MemoryStream(); +document.SerializeAsYaml(stream, AsyncApiVersion.AsyncApi3_0, settings); +``` + +A reference to a location the written document does not contain is written inline, even without `InlineReferences`. This happens when converting between versions: a 2.x reference to `#/channels/userSignup/publish/message` has no equivalent location in 3.x, and a 3.x reference to `#/operations/sendUserSignup/traits/0` has none in 2.x. References that still resolve in the written document, such as references to components or servers, stay references. AsyncAPI 2.x cannot reference tags or external documentation, so those are written inline when writing 2.x. See [version conversion](./version-conversion) for the other differences. + ## External references `AsyncApiReaderSettings.ReferenceResolution` defaults to `ReferenceResolutionSetting.ResolveInternalReferences`: local references are resolved and external references remain unresolved placeholders. Set `ResolveAllReferences` to load external references too. The built-in loader supports absolute `file://`, `http://`, and `https://` URIs. Relative external URIs require a `BaseUri`; the built-in loader resolves them relative to that URI as file paths. Other URI schemes require a custom loader. diff --git a/website/docs/guides/version-conversion.mdx b/website/docs/guides/version-conversion.mdx index 8b90c2a..6aeac5a 100644 --- a/website/docs/guides/version-conversion.mdx +++ b/website/docs/guides/version-conversion.mdx @@ -22,6 +22,19 @@ var outputV3 = document.SerializeAsJson(AsyncApiVersion.AsyncApi3_0); `AsyncApi3_0` is the API's V3 selector; the current V3 writer emits `asyncapi: 3.1.0`. -The serializer maps between the different version shapes, but a round trip is not guaranteed to preserve every representation detail. To bridge the versions, references may be moved, rewritten, or represented differently while keeping the specification's behavior as close as possible. V3-only model information cannot survive a downgrade when V2 has no corresponding field. In particular, channel keys may be normalized as addresses; generated operation keys can use `anonymous-operation-{int}` when no operation ID is available; operation IDs are not retained through a V2 downgrade; and when a V2 shape supports only one message in a location, only the first message is selected. References may also be reflowed or resolved during conversion. +The serializer maps between the different version shapes, but a round trip is not guaranteed to preserve every representation detail. To bridge the versions, references may be moved, rewritten, or represented differently while keeping the specification's behavior as close as possible. V3-only model information cannot survive a downgrade when V2 has no corresponding field. In particular, channel keys may be normalized as addresses; generated operation keys can use `anonymous-operation-{int}` when no operation ID is available; operation IDs are not retained through a V2 downgrade; and when a V2 shape supports only one message in a location, only the first message is selected. V2 has one `publish` and one `subscribe` operation per channel, so a downgrade drops further V3 operations with the same action on a channel. V2 messages only exist inside operations, so a V3 message that no operation uses is not written. Review converted output when round-trip fidelity matters, especially around removed fields, operation identifiers, multiple messages, and references. See the [AsyncAPI migration guide](https://www.asyncapi.com/docs/migration/migrating-to-v3) for specification-level changes. + +## References + +References that resolve in the written version stay references, such as references to components (`#/components/messages/userSignedUp`) or servers (`#/servers/production`). A reference to a location that does not exist in the written version is written inline instead: + +- V2 references into `publish` or `subscribe` operations, such as `#/channels/userSignup/publish/message`, have no V3 location. +- V3 references into `#/operations/...` have no V2 location. +- V3 writes component schemas as Multi Format Schema Objects, so a reference into a schema, such as `#/components/schemas/User/properties/id`, has no V3 location. +- V2 writes message headers as their schema, and cannot reference tags or external documentation, so those are written inline in V2. + +V2 keys channels by address, and the reader normalizes the address into the V3 channel key. A V2 reference to another channel, such as `#/channels/user~1signedup`, is read as `#/channels/usersignedup`, and is written back by address in V2. + +See [references](./references#write-references) for how references are written in general.