diff --git a/Frends.JSON.ConvertXMLStringToJToken/CHANGELOG.md b/Frends.JSON.ConvertXMLStringToJToken/CHANGELOG.md index c0835d4..7d06d2c 100644 --- a/Frends.JSON.ConvertXMLStringToJToken/CHANGELOG.md +++ b/Frends.JSON.ConvertXMLStringToJToken/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [2.0.0] - 2026-05-27 +### Breaking +- The `XSD` parameter has moved from the **Input** tab to the **Options** tab and is now only used (and shown) when `TypeCorrection` is `Schema`. + - **Why this is breaking:** the XSD is no longer bound where existing process references expect it. A migration (`migration.json`) ships with the package to copy `Input.XSD` → `Options.XSD` and set `TypeCorrection = Schema` automatically, but **tenants that do not apply migration.json will not migrate automatically**. On those tenants the old XSD value is dropped, so XSD-driven array mapping (added in 1.2.0) silently stops working. + - **Upgrade path (manual, for tenants without migration support):** after updating each affected process step, open the **Options** tab, set `TypeCorrection = Schema`, and paste your XSD into `Options.XSD`. This restores the previous array-mapping behaviour. Steps that never used an XSD need no changes — leave `TypeCorrection = None`. +### Added +- Added `Options` parameter with `TypeCorrection` (`None`/`Attributes`/`Schema`) to convert numeric and boolean XML values into native JSON types, via inline `xsi:type` attributes (`Attributes`) or the supplied XSD (`Schema`). Default is `None`. +### Changed +- In `Schema` mode the XSD now drives both array mapping and value typing. `Schema` mode without an XSD is a graceful no-op (returns string values) rather than an error. + ## [1.2.0] - 2026-05-07 ### Added - Added optional XSD input support to ensure consistent array/object mapping diff --git a/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken.Tests/UnitTests.cs b/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken.Tests/UnitTests.cs index 9149b9f..384f0ec 100644 --- a/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken.Tests/UnitTests.cs +++ b/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken.Tests/UnitTests.cs @@ -1,3 +1,4 @@ +using System; using Frends.JSON.ConvertXMLStringToJToken.Definitions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; @@ -25,7 +26,7 @@ public void ShouldConvertXmlStringToJToken() " }; - var result = JSON.ConvertXMLStringToJToken(input); + var result = JSON.ConvertXMLStringToJToken(input, new Options()); Assert.IsTrue(result.Success); Assert.IsInstanceOfType(result.Jtoken, typeof(JObject)); } @@ -40,7 +41,12 @@ public void ShouldUseXsdToMapSingleElementAsArray() Alan - ", + " + }; + + var options = new Options() + { + TypeCorrection = TypeCorrectionMode.Schema, XSD = @" @@ -59,7 +65,7 @@ public void ShouldUseXsdToMapSingleElementAsArray() " }; - var result = JSON.ConvertXMLStringToJToken(input); + var result = JSON.ConvertXMLStringToJToken(input, options); var root = ((JObject)result.Jtoken)["root"]; Assert.IsTrue(result.Success); @@ -73,4 +79,749 @@ public void ShouldUseXsdToMapSingleElementAsArray() Assert.AreEqual(1, persons.Count); Assert.AreEqual("Alan", persons[0]["name"]?.ToString()); } -} + + [TestMethod] + public void NoneMode_ShouldKeepValuesAsStrings() + { + var input = new Input() + { + XML = @" + 12.5 + " + }; + + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.None }); + Assert.IsTrue(result.Success); + + var price = ((JObject)result.Jtoken)["root"]?["price"] as JObject; + + // Default behaviour is unchanged: the xsi:type wrapper and string value are preserved. + Assert.IsNotNull(price); + Assert.AreEqual(JTokenType.String, price["#text"].Type); + Assert.AreEqual("12.5", price["#text"].ToString()); + } + + [TestMethod] + public void AttributesMode_ShouldConvertNumericAndBooleanValues() + { + var input = new Input() + { + XML = @" + 12.5 + 3 + 123456789012345678901234567890 + true + Widget + " + }; + + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var root = (JObject)((JObject)result.Jtoken)["root"]; + + Assert.IsTrue(result.Success); + Assert.AreEqual(JTokenType.Float, root["price"].Type); + Assert.AreEqual(12.5, root["price"].Value()); + Assert.AreEqual(JTokenType.Integer, root["quantity"].Type); + Assert.AreEqual(3, root["quantity"].Value()); + Assert.AreEqual(JTokenType.Integer, root["huge"].Type); + Assert.AreEqual(JTokenType.Boolean, root["inStock"].Type); + Assert.IsTrue(root["inStock"].Value()); + Assert.AreEqual(JTokenType.String, root["name"].Type); + } + + [TestMethod] + public void AttributesMode_ShouldKeepOtherAttributesWhileTypingText() + { + var input = new Input() + { + XML = @" + 12.5 + " + }; + + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var price = ((JObject)result.Jtoken)["root"]?["price"]; + + // Element carries another attribute, so the wrapper is preserved but #text becomes a number. + Assert.IsInstanceOfType(price, typeof(JObject)); + Assert.AreEqual("EUR", price["@currency"].ToString()); + Assert.IsNull(price["@xsi:type"]); + Assert.AreEqual(JTokenType.Float, price["#text"].Type); + Assert.AreEqual(12.5, price["#text"].Value()); + } + + [TestMethod] + public void AttributesMode_BooleanParsing_ShouldBeCaseSensitive() + { + var input = new Input() + { + XML = @" + true + TRUE + True + " + }; + + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var root = (JObject)((JObject)result.Jtoken)["root"]; + + Assert.IsTrue(result.Success); + Assert.AreEqual(JTokenType.Boolean, root["lower"].Type); + Assert.IsTrue(root["lower"].Value()); + + Assert.AreEqual(JTokenType.Boolean, root["upper"].Type); + Assert.IsTrue(root["upper"].Value()); + + Assert.AreEqual(JTokenType.Boolean, root["camel"].Type); + Assert.IsTrue(root["camel"].Value()); + } + + [TestMethod] + public void AttributesMode_ShouldHonourAliasedXsiNamespacePrefix() + { + var input = new Input() + { + XML = @" + 12.5 + true + " + }; + + // Author used 'i' instead of the conventional 'xsi' prefix; conversion must still fire + // because the prefix is resolved against the XML Schema Instance namespace URI. + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var root = (JObject)((JObject)result.Jtoken)["root"]; + + Assert.IsTrue(result.Success); + Assert.AreEqual(JTokenType.Float, root["price"].Type); + Assert.AreEqual(12.5, root["price"].Value()); + Assert.AreEqual(JTokenType.Boolean, root["inStock"].Type); + Assert.IsTrue(root["inStock"].Value()); + } + + [TestMethod] + public void AttributesMode_ShouldIgnoreTypePropertyWhenPrefixNotBoundToXsiNamespace() + { + var input = new Input() + { + XML = @" + 12.5 + " + }; + + // 'cust' is bound to an unrelated namespace, so cust:type is just an arbitrary attribute + // and must not trigger conversion. + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var price = ((JObject)result.Jtoken)["root"]?["price"]; + + Assert.IsInstanceOfType(price, typeof(JObject)); + Assert.AreEqual("float", price["@cust:type"]?.ToString()); + Assert.AreEqual(JTokenType.String, price["#text"].Type); + Assert.AreEqual("12.5", price["#text"].ToString()); + } + + [TestMethod] + public void AttributesMode_ShouldResolvePrefixDeclaredOnInnerElement() + { + var input = new Input() + { + XML = @" + + 12.5 + + " + }; + + // The xsi-equivalent prefix is bound on , not on the root — the scope must + // still be visible to the inner element when we resolve i:type. + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var price = ((JObject)result.Jtoken)["root"]?["wrapper"]?["price"]; + + Assert.AreEqual(JTokenType.Float, price.Type); + Assert.AreEqual(12.5, price.Value()); + } + + [TestMethod] + public void NoneMode_ShouldNotTouchXsiNilWrappers() + { + var input = new Input() + { + XML = @" + + + " + }; + + // In None mode the task does no type/nullability processing, so Newtonsoft's raw + // shape (wrapper objects holding @xsi:nil) is preserved verbatim. Anyone relying on + // the pre-2.0.0 output stays unaffected by the xsi:nil feature. + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.None }); + var root = (JObject)((JObject)result.Jtoken)["root"]; + + Assert.AreEqual("true", root["fax"]?["@xsi:nil"]?.ToString()); + Assert.AreEqual("false", root["phone"]?["@xsi:nil"]?.ToString()); + } + + [TestMethod] + public void AttributesMode_XsiNilTrue_EmptyElement_ShouldBecomeNull() + { + var input = new Input() + { + XML = @" + + " + }; + + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var fax = ((JObject)result.Jtoken)["root"]?["fax"]; + + Assert.IsNotNull(fax); + Assert.AreEqual(JTokenType.Null, fax.Type); + } + + [TestMethod] + public void AttributesMode_XsiNilTrue_WithContent_ShouldUseContent() + { + var input = new Input() + { + XML = @" + 123 + 42 + " + }; + + // Content wins over the nil flag: the string "123" survives, and the typed element + // still converts to integer 42 via the normal xsi:type path. + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var root = (JObject)((JObject)result.Jtoken)["root"]; + + Assert.AreEqual(JTokenType.String, root["fax"].Type); + Assert.AreEqual("123", root["fax"].ToString()); + Assert.AreEqual(JTokenType.Integer, root["count"].Type); + Assert.AreEqual(42, root["count"].Value()); + } + + [TestMethod] + public void AttributesMode_XsiNilFalse_EmptyElement_ShouldUseTypeDefault() + { + var input = new Input() + { + XML = @" + + + + + " + }; + + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var root = (JObject)((JObject)result.Jtoken)["root"]; + + Assert.AreEqual(JTokenType.Integer, root["count"].Type); + Assert.AreEqual(0L, root["count"].Value()); + Assert.AreEqual(JTokenType.Float, root["ratio"].Type); + Assert.AreEqual(0d, root["ratio"].Value()); + Assert.AreEqual(JTokenType.Boolean, root["inStock"].Type); + Assert.IsFalse(root["inStock"].Value()); + Assert.AreEqual(JTokenType.String, root["str"].Type); + Assert.AreEqual(string.Empty, root["str"].Value()); + } + + [TestMethod] + public void AttributesMode_XsiNilFalse_WithContent_ShouldStripNilAndKeepContent() + { + var input = new Input() + { + XML = @" + 5 + " + }; + + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var count = ((JObject)result.Jtoken)["root"]?["count"]; + + Assert.AreEqual(JTokenType.Integer, count.Type); + Assert.AreEqual(5, count.Value()); + } + + [TestMethod] + public void AttributesMode_XsiNilFalse_WithoutTypeInfo_ShouldBeNoOp() + { + var input = new Input() + { + XML = @" + + " + }; + + // No xsi:type → we have no type info to coerce against. Per spec, leave the wrapper + // alone (xsi:nil attribute stays so the caller can see the marker if they want). + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var fax = ((JObject)result.Jtoken)["root"]?["fax"]; + + Assert.IsInstanceOfType(fax, typeof(JObject)); + Assert.AreEqual("false", fax["@xsi:nil"].ToString()); + } + + [TestMethod] + public void AttributesMode_XsiNilFalse_PreservesOtherAttributes() + { + var input = new Input() + { + XML = @" + + " + }; + + // Element carries another data attribute, so the wrapper must survive. xsi:nil and + // xsi:type are consumed; #text becomes the typed default; @currency stays untouched. + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var price = ((JObject)result.Jtoken)["root"]?["price"]; + + Assert.IsInstanceOfType(price, typeof(JObject)); + Assert.AreEqual("EUR", price["@currency"].ToString()); + Assert.IsNull(price["@xsi:type"]); + Assert.IsNull(price["@xsi:nil"]); + Assert.AreEqual(JTokenType.Float, price["#text"].Type); + Assert.AreEqual(0d, price["#text"].Value()); + } + + [TestMethod] + public void AttributesMode_XsiNilFalse_HonoursAliasedPrefix() + { + var input = new Input() + { + XML = @" + + " + }; + + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var count = ((JObject)result.Jtoken)["root"]?["count"]; + + Assert.AreEqual(JTokenType.Integer, count.Type); + Assert.AreEqual(0L, count.Value()); + } + + [TestMethod] + public void SchemaMode_XsiNilFalse_EmptyStringElement_ShouldBecomeEmptyString() + { + var input = new Input() + { + XML = @" + + " + }; + + var options = new Options() + { + TypeCorrection = TypeCorrectionMode.Schema, + XSD = @" + + + + + + + + " + }; + + // Schema declares the element as xs:string. xsi:nil='false' on an empty element means + // "do not allow null" — for a string that's an empty string. + var result = JSON.ConvertXMLStringToJToken(input, options); + var name = ((JObject)result.Jtoken)["root"]?["name"]; + + Assert.IsNotNull(name); + Assert.AreEqual(JTokenType.String, name.Type); + Assert.AreEqual(string.Empty, name.ToString()); + } + + [TestMethod] + public void SchemaMode_XsiNilTrue_EmptyNillableElement_ShouldBecomeNull() + { + var input = new Input() + { + XML = @" + + " + }; + + var options = new Options() + { + TypeCorrection = TypeCorrectionMode.Schema, + XSD = @" + + + + + + + + " + }; + + var result = JSON.ConvertXMLStringToJToken(input, options); + var fax = ((JObject)result.Jtoken)["root"]?["fax"]; + + Assert.IsNotNull(fax); + Assert.AreEqual(JTokenType.Null, fax.Type); + } + + [TestMethod] + public void AttributesMode_ShouldLeaveUnparseableValuesAsStrings() + { + var input = new Input() + { + XML = @" + not-a-number + " + }; + + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Attributes }); + var price = ((JObject)result.Jtoken)["root"]?["price"]; + + Assert.IsInstanceOfType(price, typeof(JObject)); + Assert.AreEqual(JTokenType.String, price["#text"].Type); + Assert.AreEqual("not-a-number", price["#text"].ToString()); + } + + [TestMethod] + public void SchemaMode_ShouldConvertValuesUsingXsdTypes() + { + var input = new Input() + { + XML = @" + 12.5 + 3 + true + Widget + " + }; + + var options = new Options() + { + TypeCorrection = TypeCorrectionMode.Schema, + XSD = @" + + + + + + + + + + + " + }; + + var result = JSON.ConvertXMLStringToJToken(input, options); + Assert.IsTrue(result.Success); + + var root = ((JObject)result.Jtoken)["root"] as JObject; + Assert.IsNotNull(root); + + Assert.AreEqual(JTokenType.Float, root["price"].Type); + Assert.AreEqual(12.5, root["price"].Value()); + Assert.AreEqual(JTokenType.Integer, root["quantity"].Type); + Assert.AreEqual(JTokenType.Boolean, root["inStock"].Type); + Assert.IsTrue(root["inStock"].Value()); + Assert.AreEqual(JTokenType.String, root["name"].Type); + } + + [TestMethod] + public void SchemaMode_ShouldConvertSingleElementArraysToTypedArrays() + { + var input = new Input() + { + XML = @" + 9.5 + " + }; + + var options = new Options() + { + TypeCorrection = TypeCorrectionMode.Schema, + XSD = @" + + + + + + + + " + }; + + var result = JSON.ConvertXMLStringToJToken(input, options); + var scores = ((JObject)result.Jtoken)["root"]?["score"] as JArray; + + Assert.IsNotNull(scores); + Assert.AreEqual(1, scores.Count); + Assert.AreEqual(JTokenType.Float, scores[0].Type); + Assert.AreEqual(9.5, scores[0].Value()); + } + + [TestMethod] + public void AttributesMode_ThrowAction_ShouldThrowOnUnparseableValue() + { + var input = new Input() + { + XML = @" + not-a-number + " + }; + + var options = new Options + { + TypeCorrection = TypeCorrectionMode.Attributes, + ActionOnBadValues = BadValueAction.Throw, + }; + + var ex = Assert.ThrowsException( + () => JSON.ConvertXMLStringToJToken(input, options)); + + StringAssert.Contains(ex.Message, "not-a-number"); + StringAssert.Contains(ex.Message, "float"); + } + + [TestMethod] + public void AttributesMode_ThrowAction_ShouldNotThrowWhenAllValuesParse() + { + var input = new Input() + { + XML = @" + 12.5 + true + " + }; + + var options = new Options + { + TypeCorrection = TypeCorrectionMode.Attributes, + ActionOnBadValues = BadValueAction.Throw, + }; + + var result = JSON.ConvertXMLStringToJToken(input, options); + var root = (JObject)((JObject)result.Jtoken)["root"]; + + Assert.IsTrue(result.Success); + Assert.AreEqual(JTokenType.Float, root["price"].Type); + Assert.AreEqual(JTokenType.Boolean, root["inStock"].Type); + } + + [TestMethod] + public void AttributesMode_ThrowAction_ShouldThrowOnUnparseableBoolean() + { + var input = new Input() + { + XML = @" + maybe + " + }; + + var options = new Options + { + TypeCorrection = TypeCorrectionMode.Attributes, + ActionOnBadValues = BadValueAction.Throw, + }; + + var ex = Assert.ThrowsException( + () => JSON.ConvertXMLStringToJToken(input, options)); + + StringAssert.Contains(ex.Message, "maybe"); + StringAssert.Contains(ex.Message, "boolean"); + } + + [TestMethod] + public void AttributesMode_IgnoreAction_IsDefaultAndKeepsUnparseableAsString() + { + var input = new Input() + { + XML = @" + not-a-number + " + }; + + var options = new Options + { + TypeCorrection = TypeCorrectionMode.Attributes, + }; + + Assert.AreEqual(BadValueAction.Ignore, options.ActionOnBadValues); + + var result = JSON.ConvertXMLStringToJToken(input, options); + var price = ((JObject)result.Jtoken)["root"]?["price"]; + + Assert.IsInstanceOfType(price, typeof(JObject)); + Assert.AreEqual(JTokenType.String, price["#text"].Type); + Assert.AreEqual("not-a-number", price["#text"].ToString()); + } + + [TestMethod] + public void AttributesMode_ThrowAction_ShouldNotThrowOnUnknownXsiType() + { + var input = new Input() + { + XML = @" + DEADBEEF + " + }; + + var options = new Options + { + TypeCorrection = TypeCorrectionMode.Attributes, + ActionOnBadValues = BadValueAction.Throw, + }; + + // xsi:types we don't know how to convert (e.g. hexBinary) are not treated as "bad" — + // they're simply left alone regardless of the action setting. + var result = JSON.ConvertXMLStringToJToken(input, options); + + Assert.IsTrue(result.Success); + } + + [TestMethod] + public void SchemaMode_ShouldHonourAliasedXsiNamespacePrefix() + { + var input = new Input() + { + XML = @" + 12.5 + " + }; + + var options = new Options() + { + TypeCorrection = TypeCorrectionMode.Schema, + XSD = @" + + + + + + + + " + }; + + // Root already binds the XSD-instance namespace under prefix 'i'. The schema-injected + // type attribute should serialize as @i:type and the prefix-agnostic lookup must still + // resolve it for conversion. + var result = JSON.ConvertXMLStringToJToken(input, options); + var price = ((JObject)result.Jtoken)["root"]?["price"]; + + Assert.IsTrue(result.Success); + Assert.AreEqual(JTokenType.Float, price.Type); + Assert.AreEqual(12.5, price.Value()); + } + + [TestMethod] + public void SchemaMode_ShouldOverrideInlineXsiTypeWithSchemaType() + { + var input = new Input() + { + XML = @" + 12.5 + " + }; + + var options = new Options() + { + TypeCorrection = TypeCorrectionMode.Schema, + XSD = @" + + + + + + + + " + }; + + // The author lied with xsi:type='int' on a 12.5 value; schema says float. XSD wins. + var result = JSON.ConvertXMLStringToJToken(input, options); + var price = ((JObject)result.Jtoken)["root"]?["price"]; + + Assert.IsTrue(result.Success); + Assert.AreEqual(JTokenType.Float, price.Type); + Assert.AreEqual(12.5, price.Value()); + } + + [TestMethod] + public void SchemaMode_ShouldStripInlineXsiTypeWhenSchemaTypeIsString() + { + var input = new Input() + { + XML = @" + 42 + " + }; + + var options = new Options() + { + TypeCorrection = TypeCorrectionMode.Schema, + XSD = @" + + + + + + + + " + }; + + // Inline xsi:type='int' must not override the schema's xs:string declaration. + var result = JSON.ConvertXMLStringToJToken(input, options); + var name = ((JObject)result.Jtoken)["root"]?["name"]; + + Assert.IsTrue(result.Success); + Assert.AreEqual(JTokenType.String, name.Type); + Assert.AreEqual("42", name.ToString()); + } + + [TestMethod] + public void SchemaMode_WithoutXsd_ShouldNoOpAndKeepStrings() + { + var input = new Input() + { + XML = "12.5" + }; + + // Schema mode without an XSD is a graceful no-op (not an error), so migrated + // processes that land in Schema mode without a schema keep their previous output. + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Schema }); + Assert.IsTrue(result.Success); + + var price = ((JObject)result.Jtoken)["root"]?["price"]; + Assert.IsNotNull(price); + Assert.AreEqual(JTokenType.String, price.Type); + Assert.AreEqual("12.5", price.ToString()); + } + + [TestMethod] + public void SchemaMode_WithoutXsd_ShouldNotConvertInlineXsiTypes() + { + var input = new Input() + { + XML = @" + 12.5 + " + }; + + // Schema-without-XSD is a true no-op: inline xsi:type must not bleed through and + // start converting values either. Tenants who land here via migration with no XSD + // should see exactly the pre-2.0.0 output shape. + var result = JSON.ConvertXMLStringToJToken(input, new Options { TypeCorrection = TypeCorrectionMode.Schema }); + var price = ((JObject)result.Jtoken)["root"]?["price"]; + + Assert.IsTrue(result.Success); + Assert.IsInstanceOfType(price, typeof(JObject)); + Assert.AreEqual("float", price["@xsi:type"]?.ToString()); + Assert.AreEqual(JTokenType.String, price["#text"].Type); + Assert.AreEqual("12.5", price["#text"].ToString()); + } +} \ No newline at end of file diff --git a/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/ConvertXMLStringToJToken.cs b/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/ConvertXMLStringToJToken.cs index e9731f4..e203d34 100644 --- a/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/ConvertXMLStringToJToken.cs +++ b/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/ConvertXMLStringToJToken.cs @@ -1,9 +1,13 @@ using Frends.JSON.ConvertXMLStringToJToken.Definitions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Globalization; using System.IO; using System.Linq; +using System.Numerics; using System.Xml; using System.Xml.Linq; using System.Xml.Schema; @@ -16,21 +20,75 @@ namespace Frends.JSON.ConvertXMLStringToJToken; public class JSON { private const string JsonNamespace = "http://james.newtonking.com/projects/json"; + private const string XsiNamespace = "http://www.w3.org/2001/XMLSchema-instance"; + + private const string TextPropertyName = "#text"; + private const string XmlnsPrefixSeparator = "@xmlns:"; + + private static readonly IReadOnlyDictionary EmptyXmlnsScope = + new Dictionary(0, StringComparer.Ordinal); + + /// + /// Maps an XSD type local name to a parser producing a native JSON value, or null when unparseable. + /// + private static readonly Dictionary> TypeConverters = + new(StringComparer.OrdinalIgnoreCase) + { + ["float"] = ParseFloatingPoint, + ["double"] = ParseFloatingPoint, + ["decimal"] = ParseDecimal, + ["int"] = ParseInteger, + ["integer"] = ParseInteger, + ["long"] = ParseInteger, + ["short"] = ParseInteger, + ["byte"] = ParseInteger, + ["unsignedInt"] = ParseInteger, + ["unsignedLong"] = ParseInteger, + ["unsignedShort"] = ParseInteger, + ["unsignedByte"] = ParseInteger, + ["nonNegativeInteger"] = ParseInteger, + ["positiveInteger"] = ParseInteger, + ["nonPositiveInteger"] = ParseInteger, + ["negativeInteger"] = ParseInteger, + ["boolean"] = ParseBoolean, + }; /// /// Convert XML string to JToken. /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends.JSON.ConvertXMLStringToJToken) /// /// Input parameters + /// Optional parameters /// Object { bool Success, object Jtoken } - public static Result ConvertXMLStringToJToken([PropertyTab] Input input) + public static Result ConvertXMLStringToJToken([PropertyTab] Input input, [PropertyTab] Options options) { - var doc = string.IsNullOrWhiteSpace(input.XSD) - ? LoadXmlDocument(input.XML) - : LoadXmlDocumentWithSchemaHints(input.XML, input.XSD); + options ??= new Options(); + + // Schema mode without an XSD is treated as a no-op rather than an error so that + // processes migrated into Schema mode without a schema keep their previous output. + var useSchema = options.TypeCorrection == TypeCorrectionMode.Schema + && !string.IsNullOrWhiteSpace(options.XSD); + + var doc = useSchema + ? LoadXmlDocumentWithSchemaHints(input.XML, options.XSD, options.TypeCorrection) + : LoadXmlDocument(input.XML); var jsonString = JsonConvert.SerializeXmlNode(doc); - return new Result(true, JToken.Parse(jsonString)); + var token = JToken.Parse(jsonString); + + // Attributes always runs; Schema mode only runs when it has an XSD to derive types from, + // so picking Schema with a blank XSD stays a true no-op even if the input XML happens to + // carry inline xsi:type attributes. + var effectiveMode = useSchema + ? TypeCorrectionMode.Schema + : options.TypeCorrection == TypeCorrectionMode.Attributes + ? TypeCorrectionMode.Attributes + : TypeCorrectionMode.None; + + if (effectiveMode != TypeCorrectionMode.None) + CorrectTypes(token, options.ActionOnBadValues, effectiveMode); + + return new Result(true, token); } private static XmlDocument LoadXmlDocument(string xml) @@ -40,12 +98,18 @@ private static XmlDocument LoadXmlDocument(string xml) return doc; } - private static XmlDocument LoadXmlDocumentWithSchemaHints(string xml, string xsd) + private static XmlDocument LoadXmlDocumentWithSchemaHints(string xml, string xsd, TypeCorrectionMode typeCorrection) { var schemaSet = CreateSchemaSet(xsd); var xDocument = XDocument.Parse(xml); + // In Schema mode the XSD is the single source of truth; pull any author-written + // xsi:type off the document up front so it can't trip the validator (an unqualified + // xsi:type='int' is otherwise rejected before we ever get to AddSchemaHints) or shadow + // a schema-derived hint. + RemoveInlineXsiTypeAttributes(xDocument); + xDocument.Validate( schemaSet, (sender, args) => @@ -56,7 +120,7 @@ private static XmlDocument LoadXmlDocumentWithSchemaHints(string xml, string xsd }, true); - AddJsonArrayAttributesFromSchema(xDocument); + AddSchemaHints(xDocument, typeCorrection); var xmlDocument = new XmlDocument(); @@ -66,6 +130,22 @@ private static XmlDocument LoadXmlDocumentWithSchemaHints(string xml, string xsd return xmlDocument; } + /// + /// Removes any xsi:type attribute (under any prefix bound to the XML Schema Instance + /// namespace) from every element in the document. + /// + private static void RemoveInlineXsiTypeAttributes(XDocument document) + { + if (document.Root == null) + return; + + XNamespace xsiNs = XsiNamespace; + var xsiType = xsiNs + "type"; + + foreach (var element in document.Root.DescendantsAndSelf()) + element.Attribute(xsiType)?.Remove(); + } + private static XmlSchemaSet CreateSchemaSet(string xsd) { var schemaSet = new XmlSchemaSet(); @@ -75,35 +155,443 @@ private static XmlSchemaSet CreateSchemaSet(string xsd) return schemaSet; } - private static void AddJsonArrayAttributesFromSchema(XDocument document) + /// + /// Walks the validated document once, tagging elements that the schema declares as + /// repeatable with json:Array, and (when requested) numeric/boolean elements with xsi:type + /// so they can be converted to native JSON types after serialization. + /// + private static void AddSchemaHints(XDocument document, TypeCorrectionMode typeCorrection) { if (document.Root == null) return; XNamespace jsonNs = JsonNamespace; + XNamespace xsiNs = XsiNamespace; var hasArray = false; + var hasType = false; foreach (var element in document.Root.DescendantsAndSelf()) { - var schemaElement = element.GetSchemaInfo()?.SchemaElement; + var schemaInfo = element.GetSchemaInfo(); + var schemaElement = schemaInfo?.SchemaElement; if (schemaElement?.MaxOccurs > 1m) { element.SetAttributeValue(jsonNs + "Array", "true"); hasArray = true; } + + if (typeCorrection == TypeCorrectionMode.Schema) + { + var typeName = GetConvertibleTypeName(schemaInfo?.SchemaType); + if (typeName != null) + { + element.SetAttributeValue(xsiNs + "type", typeName); + hasType = true; + } + } + } + + if (hasArray && !IsNamespaceDeclared(document.Root, JsonNamespace)) + document.Root.SetAttributeValue(XNamespace.Xmlns + "json", JsonNamespace); + + // Pin the "xsi" prefix so injected type hints serialize as @xsi:type rather than an + // auto-generated prefix that the post-serialization conversion would not recognise. + if (hasType && !IsNamespaceDeclared(document.Root, XsiNamespace)) + document.Root.SetAttributeValue(XNamespace.Xmlns + "xsi", XsiNamespace); + } + + private static bool IsNamespaceDeclared(XElement root, string namespaceName) + { + return root.Attributes().Any(a => a.IsNamespaceDeclaration && a.Value == namespaceName); + } + + /// + /// Returns a canonical XSD type name (matching ) for numeric/boolean + /// schema types, or null for types that should remain strings. + /// + private static string GetConvertibleTypeName(XmlSchemaType schemaType) + { + return schemaType?.TypeCode switch + { + XmlTypeCode.Float => "float", + XmlTypeCode.Double => "double", + XmlTypeCode.Decimal => "decimal", + XmlTypeCode.Boolean => "boolean", + XmlTypeCode.Integer + or XmlTypeCode.NonPositiveInteger + or XmlTypeCode.NegativeInteger + or XmlTypeCode.Long + or XmlTypeCode.Int + or XmlTypeCode.Short + or XmlTypeCode.Byte + or XmlTypeCode.NonNegativeInteger + or XmlTypeCode.UnsignedLong + or XmlTypeCode.UnsignedInt + or XmlTypeCode.UnsignedShort + or XmlTypeCode.UnsignedByte + or XmlTypeCode.PositiveInteger => "integer", + _ => null, + }; + } + + /// + /// Recursively converts values carrying an xsi:type hint into native JSON numbers/booleans, + /// removing the consumed xsi:type attribute and collapsing the wrapper object when only the + /// converted value remains. + /// + private static void CorrectTypes(JToken token, BadValueAction actionOnBadValues, TypeCorrectionMode mode) + { + CorrectTypes(token, actionOnBadValues, mode, EmptyXmlnsScope); + } + + private static void CorrectTypes( + JToken token, + BadValueAction actionOnBadValues, + TypeCorrectionMode mode, + IReadOnlyDictionary xmlnsScope) + { + switch (token) + { + case JArray array: + foreach (var item in array.Children().ToList()) + CorrectTypes(item, actionOnBadValues, mode, xmlnsScope); + break; + + case JObject obj: + var childScope = ExtendXmlnsScope(obj, xmlnsScope); + foreach (var value in obj.PropertyValues().ToList()) + CorrectTypes(value, actionOnBadValues, mode, childScope); + + // xsi:nil handling runs first so it can collapse the wrapper to a default/null + // (or strip the attribute so ApplyTypeHint sees a clean element). + if (ApplyXsiNil(obj, mode, childScope)) + break; + + ApplyTypeHint(obj, actionOnBadValues, childScope); + break; } + } - var existing = document.Root.Attributes() - .FirstOrDefault(a => - a.IsNamespaceDeclaration && - a.Value == JsonNamespace); + /// + /// Extends an xmlns prefix-to-namespace map with any @xmlns:* declarations carried on this + /// JObject, so that nested elements can resolve type-hint prefixes (e.g. xmlns:i bound here, + /// i:type used on a descendant). Inner declarations shadow outer ones, matching XML scope. + /// + private static IReadOnlyDictionary ExtendXmlnsScope( + JObject obj, + IReadOnlyDictionary parentScope) + { + Dictionary extended = null; - if (hasArray && existing == null) + foreach (var prop in obj.Properties()) { - document.Root.SetAttributeValue( - XNamespace.Xmlns + "json", - JsonNamespace); + if (!prop.Name.StartsWith(XmlnsPrefixSeparator, StringComparison.Ordinal)) + continue; + + extended ??= new Dictionary(parentScope, StringComparer.Ordinal); + var prefix = prop.Name.Substring(XmlnsPrefixSeparator.Length); + extended[prefix] = prop.Value.ToString(); } + + return extended ?? parentScope; + } + + private static void ApplyTypeHint( + JObject obj, + BadValueAction actionOnBadValues, + IReadOnlyDictionary xmlnsScope) + { + var typeProperty = FindXsiTypeProperty(obj, xmlnsScope); + if (typeProperty == null) + return; + + var typeName = LocalName(typeProperty.Value.ToString()); + if (!TypeConverters.TryGetValue(typeName, out var convert)) + return; + + var textProperty = obj.Property(TextPropertyName, StringComparison.Ordinal); + if (textProperty == null || textProperty.Value.Type != JTokenType.String) + return; + + var rawValue = textProperty.Value.ToString(); + var converted = convert(rawValue); + if (converted == null) + { + if (actionOnBadValues == BadValueAction.Throw) + throw new FormatException( + $"Value '{rawValue}' on <{obj.Path}> could not be converted to '{typeName}'."); + return; + } + + typeProperty.Remove(); + + var remaining = obj.Properties().ToList(); + var onlyText = remaining.Count == 1 && remaining[0].Name == TextPropertyName; + var onlyTextAndXsiNs = remaining.Count == 2 && + remaining.Any(p => p.Name == TextPropertyName) && + remaining.Any(IsXsiNamespaceDeclaration); + + if ((onlyText || onlyTextAndXsiNs) && obj.Parent != null) + obj.Replace(converted); + else + textProperty.Value = converted; + } + + private static JProperty FindXsiTypeProperty(JObject obj, IReadOnlyDictionary xmlnsScope) => + FindXsiNamespacedAttribute(obj, "type", xmlnsScope); + + private static JProperty FindXsiNilProperty(JObject obj, IReadOnlyDictionary xmlnsScope) => + FindXsiNamespacedAttribute(obj, "nil", xmlnsScope); + + /// + /// Finds the first attribute on this JObject named "@<prefix>:<localName>" where + /// <prefix> is bound (in the active xmlns scope) to the XML Schema Instance namespace. + /// Newtonsoft preserves the source prefix, so authors using xmlns:i (instead of the + /// conventional xsi) still get their type/nil hints honoured. + /// + private static JProperty FindXsiNamespacedAttribute( + JObject obj, + string localName, + IReadOnlyDictionary xmlnsScope) + { + foreach (var prop in obj.Properties()) + { + if (prop.Name.Length < 2 || prop.Name[0] != '@') + continue; + + var colonIndex = prop.Name.IndexOf(':'); + if (colonIndex < 0) + continue; + + var local = prop.Name.Substring(colonIndex + 1); + if (!string.Equals(local, localName, StringComparison.Ordinal)) + continue; + + var prefix = prop.Name.Substring(1, colonIndex - 1); + if (xmlnsScope.TryGetValue(prefix, out var ns) && + string.Equals(ns, XsiNamespace, StringComparison.Ordinal)) + return prop; + } + + return null; + } + + /// + /// Applies xsi:nil semantics. Returns true when the JObject was replaced/finalised and no + /// further type-hint processing is needed on it. + /// nil="true" empty → JSON null (replacing the wrapper). + /// nil="true" with content → strip the attribute, let content flow through. + /// nil="false" empty → coerce to default(T) (T from xsi:type, or "" when Schema mode + /// validated the element without providing a convertible type; Attributes mode without + /// xsi:type is left alone). + /// nil="false" with content → strip the attribute, let content flow through. + /// + private static bool ApplyXsiNil( + JObject obj, + TypeCorrectionMode mode, + IReadOnlyDictionary xmlnsScope) + { + var nilProperty = FindXsiNilProperty(obj, xmlnsScope); + if (nilProperty == null) + return false; + + var nilFlag = ParseNilFlag(nilProperty.Value.ToString()); + if (nilFlag == null) + return false; + + var textProperty = obj.Property(TextPropertyName, StringComparison.Ordinal); + if (HasContent(obj, textProperty)) + { + // Content wins — drop the nil attribute. If the wrapper now has nothing else + // meaningful to carry (no xsi:type for further conversion, no other attributes), + // collapse it to the bare text value so the shape matches an authored element + // that never had xsi:nil. + nilProperty.Remove(); + + var typeProperty = FindXsiTypeProperty(obj, xmlnsScope); + if (typeProperty == null && textProperty != null && + obj.Parent != null && OnlyTextOrNamespaceDeclarations(obj)) + { + obj.Replace(textProperty.Value); + return true; + } + + return false; + } + + // Empty element — handle the nil flag. + if (nilFlag == true) + { + nilProperty.Remove(); + ReplaceOrClearWrapper(obj, textProperty, JValue.CreateNull()); + return true; + } + + // nil = false on an empty element: coerce to default(T), but only when we actually + // have type info to derive T from. + var typePropertyForDefault = FindXsiTypeProperty(obj, xmlnsScope); + JValue defaultValue; + + if (typePropertyForDefault != null) + { + defaultValue = DefaultValueForTypeName(LocalName(typePropertyForDefault.Value.ToString())); + typePropertyForDefault.Remove(); + } + else if (mode == TypeCorrectionMode.Schema) + { + // The schema validated this element; a missing xsi:type means the schema's type is + // a non-convertible one (xs:string or similar). Default to an empty string. + defaultValue = new JValue(string.Empty); + } + else + { + // Attributes mode with no inline xsi:type: per design, only act when type info is + // present. Leave xsi:nil in place so the caller can still see the marker. + return false; + } + + nilProperty.Remove(); + ReplaceOrClearWrapper(obj, textProperty, defaultValue); + return true; + } + + private static bool OnlyTextOrNamespaceDeclarations(JObject obj) + { + foreach (var prop in obj.Properties()) + { + if (prop.Name == TextPropertyName) + continue; + if (prop.Name.StartsWith("@xmlns", StringComparison.Ordinal)) + continue; + return false; + } + return true; + } + + private static bool? ParseNilFlag(string value) + { + var trimmed = value?.Trim() ?? string.Empty; + + if (trimmed == "1" || string.Equals(trimmed, "true", StringComparison.OrdinalIgnoreCase)) + return true; + + if (trimmed == "0" || string.Equals(trimmed, "false", StringComparison.OrdinalIgnoreCase)) + return false; + + return null; + } + + private static bool HasContent(JObject obj, JProperty textProperty) + { + if (textProperty != null && textProperty.Value.Type == JTokenType.String && + !string.IsNullOrEmpty(textProperty.Value.ToString())) + return true; + + foreach (var prop in obj.Properties()) + { + if (prop.Name.Length == 0 || prop.Name[0] == '@' || prop.Name == TextPropertyName) + continue; + + return true; + } + + return false; + } + + /// + /// When the wrapper's only remaining properties are namespace declarations, collapse it to + /// the new scalar value. Otherwise preserve the wrapper (other attributes are still + /// meaningful) and assign the value to #text. + /// + private static void ReplaceOrClearWrapper(JObject obj, JProperty existingTextProperty, JValue newValue) + { + var canCollapse = obj.Parent != null && + obj.Properties().All(p => p.Name.StartsWith("@xmlns", StringComparison.Ordinal)); + + if (canCollapse) + { + obj.Replace(newValue); + return; + } + + if (existingTextProperty != null) + existingTextProperty.Value = newValue; + else + obj[TextPropertyName] = newValue; + } + + /// + /// Mirrors C# default(T) for the recognized XSD-ish type names; unknown types fall back to + /// an empty string so callers always get a usable JSON value. + /// + private static JValue DefaultValueForTypeName(string typeName) + { + if (string.IsNullOrEmpty(typeName) || !TypeConverters.ContainsKey(typeName)) + return new JValue(string.Empty); + + return typeName.ToLowerInvariant() switch + { + "float" or "double" => new JValue(0d), + "decimal" => new JValue(0m), + "boolean" => new JValue(false), + _ => new JValue(0L), + }; + } + + private static bool IsXsiNamespaceDeclaration(JProperty property) + { + return property.Name.StartsWith("@xmlns", StringComparison.Ordinal) && + string.Equals(property.Value.ToString(), XsiNamespace, StringComparison.Ordinal); + } + + private static string LocalName(string value) + { + var index = value.IndexOf(':'); + return index >= 0 ? value[(index + 1)..] : value; + } + + private static JValue ParseFloatingPoint(string value) + { + switch (value.Trim()) + { + case "INF": return new JValue(double.PositiveInfinity); + case "-INF": return new JValue(double.NegativeInfinity); + case "NaN": return new JValue(double.NaN); + } + + return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) + ? new JValue(parsed) + : null; + } + + private static JValue ParseDecimal(string value) + { + return decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) + ? new JValue(parsed) + : null; + } + + private static JValue ParseInteger(string value) + { + if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLong)) + return new JValue(parsedLong); + + return BigInteger.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBig) + ? new JValue(parsedBig) + : null; + } + + private static JValue ParseBoolean(string value) + { + var trimmed = value.Trim(); + + if (trimmed == "1" || string.Equals(trimmed, "true", StringComparison.OrdinalIgnoreCase)) + return new JValue(true); + + if (trimmed == "0" || string.Equals(trimmed, "false", StringComparison.OrdinalIgnoreCase)) + return new JValue(false); + + return null; } } diff --git a/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/Definitions/Input.cs b/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/Definitions/Input.cs index 2f21d84..70116d6 100644 --- a/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/Definitions/Input.cs +++ b/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/Definitions/Input.cs @@ -10,13 +10,4 @@ public class Input /// /// <?xml version='1.0' standalone='no'?><root><foos id = '1' ><foo>bar</name></foos></root> public string XML { get; set; } - - /// - /// Optional XSD schema used for XML validation and for determining - /// whether XML elements should be serialized as JSON arrays. - /// - /// - /// <xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>...</xs:schema> - /// - public string XSD { get; set; } } diff --git a/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/Definitions/Options.cs b/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/Definitions/Options.cs new file mode 100644 index 0000000..41abe0b --- /dev/null +++ b/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/Definitions/Options.cs @@ -0,0 +1,80 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Frends.JSON.ConvertXMLStringToJToken.Definitions; + +/// +/// Controls how XML values are mapped to native JSON types. +/// +public enum TypeCorrectionMode +{ + /// + /// No type correction. Every value is emitted as a JSON string (default, backwards compatible). + /// + None, + + /// + /// Read inline xsi:type attributes from the XML (e.g. xsi:type="float") to convert + /// numeric and boolean values into native JSON types. + /// + Attributes, + + /// + /// Derive value types from the provided XSD schema (xs:float, xs:int, xs:boolean, ...) + /// to convert numeric and boolean values into native JSON types. Requires an XSD. + /// + Schema, +} + +/// +/// Controls what happens when a value carries a known numeric/boolean type hint but cannot +/// be parsed as that type (e.g. xsi:type="int" with value "abc"). +/// +public enum BadValueAction +{ + /// + /// Leave the unparseable value as a JSON string (default, backwards compatible). + /// + Ignore, + + /// + /// Throw an exception identifying the element and value that failed to convert. + /// + Throw, +} + +/// +/// Optional parameters. +/// +public class Options +{ + /// + /// Whether and how to convert string values into native JSON numbers and booleans. + /// None leaves all values as strings (default). Attributes reads inline xsi:type hints + /// from the XML. Schema derives the types from the supplied XSD. + /// + /// TypeCorrectionMode.Attributes + [DefaultValue(TypeCorrectionMode.None)] + public TypeCorrectionMode TypeCorrection { get; set; } = TypeCorrectionMode.None; + + /// + /// XSD schema used to validate the XML and to derive value types and array mapping. + /// Only used (and shown) when TypeCorrection is Schema. + /// + /// + /// <xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>...</xs:schema> + /// + [UIHint(nameof(TypeCorrection), "", TypeCorrectionMode.Schema)] + public string XSD { get; set; } + + /// + /// What to do when a value carries a recognized numeric/boolean type hint but cannot be + /// parsed as that type (e.g. xsi:type="int" with value "abc"). Ignore (default) keeps the + /// unparseable value as a JSON string. Throw raises an exception identifying the element + /// and value. Only used (and shown) when TypeCorrection is Attributes or Schema. + /// + /// BadValueAction.Throw + [DefaultValue(BadValueAction.Ignore)] + [UIHint(nameof(TypeCorrection), "", TypeCorrectionMode.Attributes, TypeCorrectionMode.Schema)] + public BadValueAction ActionOnBadValues { get; set; } = BadValueAction.Ignore; +} diff --git a/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken.csproj b/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken.csproj index a79bb1a..feb23d9 100644 --- a/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken.csproj +++ b/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken.csproj @@ -2,7 +2,7 @@ net6.0 - 1.2.0 + 2.0.0 Frends Frends Frends @@ -19,6 +19,8 @@ PreserveNewest + + diff --git a/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/migration.json b/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/migration.json new file mode 100644 index 0000000..fed46ef --- /dev/null +++ b/Frends.JSON.ConvertXMLStringToJToken/Frends.JSON.ConvertXMLStringToJToken/migration.json @@ -0,0 +1,23 @@ +[ + { + "Task": "Frends.JSON.ConvertXMLStringToJToken.JSON.ConvertXMLStringToJToken", + "Migrations": [ + { + "Version": "2.0.0", + "Description": "Moved the XSD parameter from the Input tab to the Options tab.", + "Migration": [ + { + "Type": "Copy", + "Source": "Input.XSD", + "Target": "Options.XSD" + }, + { + "Type": "Set", + "Target": "Options.TypeCorrection", + "Value": "Schema" + } + ] + } + ] + } +]