Skip to content

Fix/extension object preserve encoding typeid#3761

Merged
marcschier merged 14 commits into
OPCFoundation:masterfrom
gudeljuluri:fix/extension-object-preserve-encoding-typeid
May 20, 2026
Merged

Fix/extension object preserve encoding typeid#3761
marcschier merged 14 commits into
OPCFoundation:masterfrom
gudeljuluri:fix/extension-object-preserve-encoding-typeid

Conversation

@gudeljuluri

@gudeljuluri gudeljuluri commented May 13, 2026

Copy link
Copy Markdown
Contributor

See #3771

Proposed changes

When decoders (BinaryDecoder, JsonDecoder, XmlDecoder) successfully decode an ExtensionObject body into a typed IEncodeable, they were constructing the result with new ExtensionObject(encodeable), which sets ExtensionObject.TypeId = encodeable.TypeId — the DataType NodeId (e.g., i=887 for EUInformation).

Per OPC 10000-6 v1.05.07 §5.2.2.15 (Table 25 – ExtensionObject Binary DataEncoding), the TypeId field is:
"The identifier for the DataTypeEncoding node in the Server's AddressSpace."

This is the encoding NodeId (e.g., i=889), not the DataType NodeId. The decoders correctly read this value from the wire but then overwrote it when wrapping the decoded body.

This caused issues for consumers relying on ExtensionObject.TypeId (e.g., for type registration or lookup), and could cause BadTypeMismatch errors when writing back a previously read ExtensionObject through a code path that doesn't use the encoder's override logic.

Fix

Changed 4 call sites across 3 decoders to use new ExtensionObject(extension.TypeId, encodeable) instead of new ExtensionObject(encodeable), preserving the wire encoding NodeId:
• BinaryDecoder.ReadExtensionObject — binary body path
• BinaryDecoder.ReadExtensionObject — XML-in-binary body path
• JsonDecoder.TryGetExtensionObjectFromElement — JSON body path
• XmlDecoder.ReadExtensionObjectBody — XML body path

Note: PubSubJsonDecoder already correctly used new ExtensionObject(typeId, encodeable).

Why this is safe

The Binary and XML encoders already ignore ExtensionObject.TypeId for decoded bodies — they derive the wire TypeId from IEncodeable.BinaryEncodingId / IEncodeable.XmlEncodingId respectively. The IEncodeable body still carries all three ids (TypeId, BinaryEncodingId, XmlEncodingId), so no information is lost.

Types of changes

What types of changes does your code introduce?
Put an x in the boxes that apply. You can also fill these out after creating the PR.

  • Bugfix (non-breaking change which fixes an issue)
  • Enhancement (non-breaking change which adds functionality)
  • Test enhancement (non-breaking change to increase test coverage)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected, requires version increase of Nuget packages)
  • Documentation Update (if none of the other choices apply)

Checklist

Put an x in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.

  • I have read the CONTRIBUTING doc.
  • I have signed the CLA.
  • I ran tests locally with my changes, all passed.
  • I fixed all failing tests in the CI pipelines.
  • I fixed all introduced issues with CodeQL and LGTM.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added necessary documentation (if appropriate).
  • Any dependent changes have been merged and published in downstream modules.

Further comments

This issue was previously identified in PR #952 (April 2020), but that fix went the wrong direction — it explicitly overwrote ExtensionObject.TypeId with encodeable.TypeId (the DataType id). That was reverted in PRs #979 and #980 (May 2020), but the correct fix (preserving the wire encoding id) was never applied.

BinaryDecoder, JsonDecoder, and XmlDecoder were overwriting
ExtensionObject.TypeId with the DataType NodeId (e.g., i=887)
instead of preserving the encoding NodeId from the wire (e.g., i=889).

Use ExtensionObject(typeId, encodeable) to retain the original
encoding NodeId per OPC 10000-6 §5.2.2.15.
@CLAassistant

CLAassistant commented May 13, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@romanett

Copy link
Copy Markdown
Contributor

@gudeljuluri can you write some Tests that verifiy the correct behaviour being present with your fix?

@marcschier

Copy link
Copy Markdown
Collaborator

@gudeljuluri Could you open an issue with above info and link?

@marcschier

Copy link
Copy Markdown
Collaborator

@gudeljuluri can you write some Tests that verifiy the correct behaviour being present with your fix?

Probably as easy as finding an existing test and checking the typeid points to binary or xmlencoding identifier. That would be all that is needed IMO.

@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 72.04%. Comparing base (296a904) to head (851f96c).

Files with missing lines Patch % Lines
Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3761      +/-   ##
==========================================
+ Coverage   71.98%   72.04%   +0.06%     
==========================================
  Files         677      677              
  Lines      130754   130752       -2     
  Branches    22260    22261       +1     
==========================================
+ Hits        94123    94204      +81     
+ Misses      29963    29885      -78     
+ Partials     6668     6663       -5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@marcschier marcschier marked this pull request as draft May 13, 2026 12:15
…in XmlDecoderTests.

Introduced a test encodeable with distinct TypeId and BinaryEncodingId for validation.
@gudeljuluri

Copy link
Copy Markdown
Contributor Author

For the XMLDecoder, I used the existing test ReadExtensionObjectBodyKnownTypeReturnsEncodeable.

For the BinaryDecoder, I created a new IEncodable implementation with different type IDs (TestEncodeableWithDifferentTypeIds) and added a new test, ReadExtensionObjectPreservesEncodingTypeId. I’m not sure whether this aligns perfectly with your testing methodology, so feel free to adjust it as needed.

I also created a new issue 3771

@romanett

Copy link
Copy Markdown
Contributor

@gudeljuluri thank you this seems good👍 can you merge with master so we can get CI to pass

@romanett

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@marcschier marcschier marked this pull request as ready for review May 15, 2026 08:48
@romanett

Copy link
Copy Markdown
Contributor

@gudeljuluri can you please fix the now failing Tests?

@marcschier

Copy link
Copy Markdown
Collaborator

Lots of test failures. @gudeljuluri could you look into those? You can see those individual tests better on the azure devops pipeline, tests tab.

@marcschier marcschier marked this pull request as draft May 15, 2026 12:45
@gudeljuluri

Copy link
Copy Markdown
Contributor Author

The tests are failing mainly because the expected results were created using ExtensionObject(IEncodeable), which also copies body.TypeId into ExtensionObject.TypeId. I assume this constructor was originally intended for scenarios where the encoding type is unknown.

My fix resolves the issue on the decoder side, but this constructor is used in many places, and I’m not sure how to handle it without causing unintended side effects. I also can’t fully assess how any change to it would impact the rest of the codebase.

I’ve committed one last change that will fix some tests where the encoding type is accessible, but overall I think I should abandon this PR. Sorry for the inconvenience.

@romanett

Copy link
Copy Markdown
Contributor

@gudeljuluri thank you for the heads up, we will discuss in the working group how to proceed, and close the PR If needed.

@marcschier

marcschier commented May 15, 2026

Copy link
Copy Markdown
Collaborator

I think it would be best to change the Equals implementation of ExtensionObject type to test any combination of incoming TypeId against the TypeId and both binary and xml EncodingId of the inner IEncodeable.

The tests should them pass, it is not breaking and factually / spec correct.

@marcschier

Copy link
Copy Markdown
Collaborator

Once #3777 is in, we can merge from master and get this one in.

@marcschier marcschier marked this pull request as ready for review May 18, 2026 11:31
@marcschier

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@marcschier

Copy link
Copy Markdown
Collaborator

Final issue despite flaky tests:

TestExtensionObjectWithDynamicEncodeable

Error message
Assert.That(encodedJson, Is.EqualTo(expectedJson))
Expected string length 71 but was 78. Strings differ at index 33.
Expected: "{"Test":{"UaTypeId":"s=test_dyn2_typeid","Foo":"bar_1","Foo2"..."
But was: "{"Test":{"UaTypeId":"s=test_dyn2_xmlencodingid","Foo":"bar_1"..."
--------------------------------------------^

at Opc.Ua.Core.Tests.Types.Encoders.JsonEncoderTests.TestExtensionObjectWithDynamicEncodeable() in /_/Tests/Opc.Ua.Core.Tests/Types/Encoders/JsonEncoderTests.cs:line 1544

@gudeljuluri gudeljuluri marked this pull request as draft May 20, 2026 07:11
Per §5.4.2.16, the UaTypeId field in JSON encoding must be the NodeId of a DataType Node, not a DataTypeEncoding Node. Previously, the JsonEncoder used ExtensionObject.TypeId directly, which after the decoder fix to preserve encoding TypeIds could contain a Binary or XML encoding NodeId when re-encoding from another format.

Changed WriteExtensionObject to use encodeable.TypeId (the DataType NodeId) when the body is IEncodeable, falling back to value.TypeId only for raw bodies (ByteString, string) where no IEncodeable is available.

This ensures correct cross-encoding behavior (e.g., XML→JSON or Binary→JSON) where the ExtensionObject carries an encoding-specific TypeId from the source format but must emit the DataType NodeId in JSON.
@gudeljuluri

Copy link
Copy Markdown
Contributor Author

This failure is expected given the decoder changes. The test encodes to XML, decodes from XML (which now correctly preserves the XmlEncodingId in ExtensionObject.TypeId), then re-encodes to JSON. The JsonEncoder was using value.TypeId directly, which after the XML decode now contains the XML encoding ID instead of the DataType NodeId.

Per OPC UA Part 6 §5.4.2.16, the UaTypeId field in JSON must be the NodeId of a DataType Node. Fixed the JsonEncoder.WriteExtensionObject to use encodeable.TypeId (the DataType NodeId) when the body is IEncodeable, falling back to value.TypeId only for raw bodies. This is consistent with how BinaryEncoder already uses encodeable. BinaryEncodingId and XmlEncoder uses encodeable.XmlEncodingId — each encoder resolves the correct TypeId from the body itself, independent of what's stored in ExtensionObject.TypeId.

@gudeljuluri gudeljuluri marked this pull request as ready for review May 20, 2026 07:20
@marcschier

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@marcschier marcschier merged commit df24255 into OPCFoundation:master May 20, 2026
136 of 137 checks passed
marcschier pushed a commit that referenced this pull request May 23, 2026
Merged origin/master (GC-reduction pooling PR #3793, extension-object
encoding fix #3761).

Fixes for code that didn't conform to the enforced .editorconfig rules:
- Restored null-forgiving operators on UdpDiscoveryPublisher.cs,
  UdpPubSubConnection.cs, and Server/Subscription.cs where the
  compiler's nullable flow can't prove non-null across ref parameters
  and conditional initialization.
- Fixed NUnit4002 (Is.EqualTo(0) -> Is.Zero) in new
  PooledNotificationDispatchTests.cs.
- Suppressed CA1812 on BenchmarkDotNet InProcessConfig class
  (instantiated by reflection).

0 warnings, 0 errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants