diff --git a/Assets/VRM/UniJSON/Editor/Tests/Json/JsonSerializerTests.cs b/Assets/VRM/UniJSON/Editor/Tests/Json/JsonSerializerTests.cs index 767e55505..0f497e9fb 100644 --- a/Assets/VRM/UniJSON/Editor/Tests/Json/JsonSerializerTests.cs +++ b/Assets/VRM/UniJSON/Editor/Tests/Json/JsonSerializerTests.cs @@ -28,7 +28,7 @@ namespace UniJSON } struct EnumTest - { + { public HogeFuga EnumDefault; [JsonSchema(EnumSerializationType =EnumSerializationType.AsInt)] @@ -104,6 +104,7 @@ namespace UniJSON Assert.AreEqual(1, json.GetObjectCount()); Assert.AreEqual(1, json["Vector"][0].GetInt32()); } + #endregion #region Deserialize diff --git a/Assets/VRM/UniJSON/Editor/Tests/Json/SchemaTests.cs b/Assets/VRM/UniJSON/Editor/Tests/Json/SchemaTests.cs index 66daa11a3..1f51b07a0 100644 --- a/Assets/VRM/UniJSON/Editor/Tests/Json/SchemaTests.cs +++ b/Assets/VRM/UniJSON/Editor/Tests/Json/SchemaTests.cs @@ -1,7 +1,6 @@ #pragma warning disable 0649 using NUnit.Framework; - namespace UniJSON { public class SchemaTests @@ -40,6 +39,24 @@ namespace UniJSON Assert.AreEqual(0, parsed["properties"]["age"]["minimum"].GetInt32()); } + [JsonSchema(Title="MultipleConstraints")] + public class MultipleConstraints + { + [JsonSchema(Required = true, Minimum = 0, Maximum = 100)] + public int ranged; + } + + [Test] + public void CreateFromClassWithMultipleConstraints() + { + var s = JsonSchema.FromType(); + + var v = s.Validator as JsonObjectValidator; + var rangedV = v.Properties["ranged"].Validator as JsonIntValidator; + Assert.AreEqual(0, rangedV.Minimum); + Assert.AreEqual(100, rangedV.Maximum); + } + public enum ProjectionType { Perspective, @@ -48,7 +65,7 @@ namespace UniJSON class EnumStringTest { - [JsonSchema(EnumSerializationType =EnumSerializationType.AsLowerString)] + [JsonSchema(EnumSerializationType = EnumSerializationType.AsLowerString)] public ProjectionType type; } @@ -81,7 +98,7 @@ namespace UniJSON ] } - + } } "; @@ -117,7 +134,7 @@ namespace UniJSON ] } - + } } "; @@ -129,6 +146,5 @@ namespace UniJSON Assert.AreEqual(fromJson, fromType); } - } } diff --git a/Assets/VRM/UniJSON/Editor/Tests/Json/SerializeWithSchemaTests.cs b/Assets/VRM/UniJSON/Editor/Tests/Json/SerializeWithSchemaTests.cs new file mode 100644 index 000000000..e5aacd5cb --- /dev/null +++ b/Assets/VRM/UniJSON/Editor/Tests/Json/SerializeWithSchemaTests.cs @@ -0,0 +1,81 @@ +using NUnit.Framework; + +namespace UniJSON +{ + public class SerializeWithSchemaTests + { + [JsonSchema(Title="CheckConstraintsTest")] + public class CheckConstraintsTest + { + [JsonSchema(Minimum = 0)] + public int X; + + [JsonSchema(Minimum = 10)] // Not required, thus ignored when the value violates the constraints + public int Y; + } + + [Test] + public void TestCheckConstraints() + { + var obj = new CheckConstraintsTest() + { + X = 0, + Y = 0, // Will be excluded because 0 doesn't satisfy a requirement of "Minimum = 10" + }; + + var s = JsonSchema.FromType(); + { + var c = new JsonSchemaValidationContext(obj); + Assert.Null(s.Validator.Validate(c, s)); + } + var actual = s.Serialize(obj); + + var expected = @"{""X"":0}"; + + Assert.AreEqual(expected, actual); + } + + [JsonSchema(Title="ObjectNestedTest")] + public class ObjectNestedTest + { + public CheckConstraintsTest C; + } + + [Test] + public void TestObjectNested() + { + var obj = new ObjectNestedTest() + { + C = new CheckConstraintsTest(), + }; + + var s = JsonSchema.FromType(); + { + var c = new JsonSchemaValidationContext(obj); + Assert.Null(s.Validator.Validate(c, s)); + } + var actual = s.Serialize(obj); + + var expected = @"{""C"":{""X"":0}}"; + + Assert.AreEqual(expected, actual); + } + + [Test] + public void TestObjectNestedWithNull() + { + var obj = new ObjectNestedTest(); + + var s = JsonSchema.FromType(); + { + var c = new JsonSchemaValidationContext(obj); + Assert.Null(s.Validator.Validate(c, s)); + } + var actual = s.Serialize(obj); + + var expected = @"{}"; + + Assert.AreEqual(expected, actual); + } + } +} diff --git a/Assets/VRM/UniJSON/Editor/Tests/Json/SerializeWithSchemaTests.cs.meta b/Assets/VRM/UniJSON/Editor/Tests/Json/SerializeWithSchemaTests.cs.meta new file mode 100644 index 000000000..b0c9e7173 --- /dev/null +++ b/Assets/VRM/UniJSON/Editor/Tests/Json/SerializeWithSchemaTests.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: f1d1c3d9d4d20db409e9d5d9d671abbe +timeCreated: 1546930461 +licenseType: Pro +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/VRM/UniJSON/Editor/Tests/Json/ValidatorTests.cs b/Assets/VRM/UniJSON/Editor/Tests/Json/ValidatorTests.cs index d6a475251..3a071c619 100644 --- a/Assets/VRM/UniJSON/Editor/Tests/Json/ValidatorTests.cs +++ b/Assets/VRM/UniJSON/Editor/Tests/Json/ValidatorTests.cs @@ -100,6 +100,12 @@ namespace UniJSON { var c = new JsonSchemaValidationContext("test"); + { + var v = new JsonStringValidator(); + Assert.Null(v.Validate(c, "")); + Assert.Null(v.Validate(c, "a")); + } + { var v = new JsonStringValidator(); v.MinLength = 1; @@ -199,6 +205,89 @@ namespace UniJSON Assert.True(c.IsEmpty()); } + class NotRequired + { + [JsonSchema(Minimum = 1)] + public int Value; + } + + [Test] + public void ObjectValidatorForNotRequired() + { + { + var c = new JsonSchemaValidationContext("test") + { + EnableDiagnosisForNotRequiredFields = false, // Default behaviour + }; + + var s = JsonSchema.FromType(); + // An error is not returned because Value is not 'Required' and the diagnosis is not enabled + Assert.Null(s.Validator.Validate(c, new NotRequired { Value = 0 })); + + Assert.True(c.IsEmpty()); + } + + { + var c = new JsonSchemaValidationContext("test") + { + EnableDiagnosisForNotRequiredFields = true, + }; + + var s = JsonSchema.FromType(); + Assert.NotNull(s.Validator.Validate(c, new NotRequired { Value = 0 })); + + Assert.True(c.IsEmpty()); + } + } + + class NotRequiredWithIgnorable + { + [JsonSchema(Minimum = 2, ExplicitIgnorableValue = -1)] + public int Value; + } + + [Test] + public void ObjectValidatorForNotRequiredWithIgnorable() + { + { + var c = new JsonSchemaValidationContext("test") + { + EnableDiagnosisForNotRequiredFields = false, // Default behaviour + }; + + var s = JsonSchema.FromType(); + // An error is not returned because Value is not 'Required' and the diagnosis is not enabled + Assert.Null(s.Validator.Validate(c, new NotRequiredWithIgnorable { Value = 0 })); + + Assert.True(c.IsEmpty()); + } + + { + var c = new JsonSchemaValidationContext("test") + { + EnableDiagnosisForNotRequiredFields = true, + }; + + var s = JsonSchema.FromType(); + Assert.NotNull(s.Validator.Validate(c, new NotRequiredWithIgnorable { Value = 0 })); + + Assert.True(c.IsEmpty()); + } + + { + var c = new JsonSchemaValidationContext("test") + { + EnableDiagnosisForNotRequiredFields = true, + }; + + var s = JsonSchema.FromType(); + // An error is NOT returned even though diagnosis is enabled because of an ignorable value is matched + Assert.Null(s.Validator.Validate(c, new NotRequiredWithIgnorable { Value = -1 })); + + Assert.True(c.IsEmpty()); + } + } + [Test] public void DictionaryValidator() { @@ -247,5 +336,55 @@ namespace UniJSON Assert.True(c.IsEmpty()); } + + class HasArrayOBject + { + [ItemJsonSchema(Minimum = 0.0, Maximum = 1.0)] + public float[] xs; + } + + [Test] + public void HasArrayObjectValidator() + { + { + var c = new JsonSchemaValidationContext("test") + { + EnableDiagnosisForNotRequiredFields = true, + }; + + var s = JsonSchema.FromType(); + + Assert.Null(s.Validator.Validate(c, new HasArrayOBject { xs = new float[] {} })); + Assert.Null(s.Validator.Validate(c, new HasArrayOBject { xs = new float[] { 0.5f } })); + Assert.NotNull(s.Validator.Validate(c, new HasArrayOBject { xs = new float[] { 1.5f } })); + + Assert.True(c.IsEmpty()); + } + } + + class HasListObject + { + [ItemJsonSchema(Minimum = 0.0, Maximum = 1.0)] + public List xs; + } + + [Test] + public void HasListObjectValidator() + { + { + var c = new JsonSchemaValidationContext("test") + { + EnableDiagnosisForNotRequiredFields = true, + }; + + var s = JsonSchema.FromType(); + + Assert.Null(s.Validator.Validate(c, new HasListObject { xs = new List {} })); + Assert.Null(s.Validator.Validate(c, new HasListObject { xs = new List { 0.5f } })); + Assert.NotNull(s.Validator.Validate(c, new HasListObject { xs = new List { 1.5f } })); + + Assert.True(c.IsEmpty()); + } + } } } diff --git a/Assets/VRM/UniJSON/Scripts/FormatterExtensionsSerializer.cs b/Assets/VRM/UniJSON/Scripts/FormatterExtensionsSerializer.cs index cced736aa..1273ee582 100644 --- a/Assets/VRM/UniJSON/Scripts/FormatterExtensionsSerializer.cs +++ b/Assets/VRM/UniJSON/Scripts/FormatterExtensionsSerializer.cs @@ -47,7 +47,8 @@ namespace UniJSON } else { - typeof(FormatterExtensionsSerializer).GetMethod("Serialize").MakeGenericMethod(value.GetType()).Invoke(null, new object[] { f, value }); + typeof(FormatterExtensionsSerializer).GetMethod("Serialize") + .MakeGenericMethod(value.GetType()).Invoke(null, new object[] { f, value }); } } @@ -92,7 +93,7 @@ namespace UniJSON var mi = typeof(IFormatter).GetMethod("Value", new Type[] { t }); if (mi != null) { - // premitives + // primitives var self = Expression.Parameter(typeof(IFormatter), "f"); var arg = Expression.Parameter(t, "value"); var call = Expression.Call(self, mi, arg); @@ -164,6 +165,7 @@ namespace UniJSON return (IFormatter f, T value) => schema.Serialize(f, value); } + //throw new NotImplementedException(); } diff --git a/Assets/VRM/UniJSON/Scripts/Json/JsonSchema.cs b/Assets/VRM/UniJSON/Scripts/Json/JsonSchema.cs index 7adfe12e4..1957c4119 100644 --- a/Assets/VRM/UniJSON/Scripts/Json/JsonSchema.cs +++ b/Assets/VRM/UniJSON/Scripts/Json/JsonSchema.cs @@ -57,6 +57,9 @@ namespace UniJSON /// public bool SkipComparison { get; set; } + public object ExplicitIgnorableValue { private get; set; } + public int ExplicitIgnorableItemLength { private get; set; } + public override string ToString() { return string.Format("<{0}>", Title); @@ -175,7 +178,9 @@ namespace UniJSON Title = a.Title, Description = a.Description, Validator = validator, - SkipComparison = skipComparison + SkipComparison = skipComparison, + ExplicitIgnorableValue = a.ExplicitIgnorableValue, + ExplicitIgnorableItemLength = a.ExplicitIgnorableItemLength, }; return schema; @@ -383,9 +388,12 @@ namespace UniJSON } #endregion - public void Serialize(IFormatter f, T o) + public void Serialize(IFormatter f, T o, JsonSchemaValidationContext c = null) { - var c = new JsonSchemaValidationContext(o); + if (c == null) + { + c = new JsonSchemaValidationContext(o); + } var ex = Validator.Validate(c, o); if (ex != null) @@ -404,14 +412,30 @@ namespace UniJSON Validator.ToJsonScheama(f); f.EndMap(); } + + public bool IsExplicitlyIgnorableValue(T obj) + { + if (obj == null) + { + return ExplicitIgnorableValue == null; + } + + var iter = obj as System.Collections.ICollection; + if (ExplicitIgnorableItemLength != -1 && iter != null) + { + return iter.Count == ExplicitIgnorableItemLength; + } + + return obj.Equals(ExplicitIgnorableValue); + } } public static class JsonSchemaExtensions { - public static string Serialize(this JsonSchema s, T o) + public static string Serialize(this JsonSchema s, T o, JsonSchemaValidationContext c = null) { var f = new JsonFormatter(); - s.Serialize(f, o); + s.Serialize(f, o, c); return f.ToString(); } } diff --git a/Assets/VRM/UniJSON/Scripts/Json/JsonSchemaAttribute.cs b/Assets/VRM/UniJSON/Scripts/Json/JsonSchemaAttribute.cs index 4171769f1..4e44ebea2 100644 --- a/Assets/VRM/UniJSON/Scripts/Json/JsonSchemaAttribute.cs +++ b/Assets/VRM/UniJSON/Scripts/Json/JsonSchemaAttribute.cs @@ -55,6 +55,18 @@ namespace UniJSON /// public bool SkipSchemaComparison; + /// + /// Suppress errors if a value of the field which is not required by a schema is matched to this value. + /// This feature will be useful to ignore invalid value which is known. + /// + public object ExplicitIgnorableValue; + + /// + /// Suppress errors if length of a value of the field which is not required by a schema is matched to this value. + /// This feature will be useful to ignore invalid value which is known. + /// + public int ExplicitIgnorableItemLength = -1; + public void Merge(BaseJsonSchemaAttribute rhs) { if (rhs == null) return; diff --git a/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/IJsonSchemaValidator.cs b/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/IJsonSchemaValidator.cs index 6c500e0d3..9a53769d8 100644 --- a/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/IJsonSchemaValidator.cs +++ b/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/IJsonSchemaValidator.cs @@ -7,6 +7,8 @@ namespace UniJSON { Stack m_stack = new Stack(); + public bool EnableDiagnosisForNotRequiredFields = false; + public JsonSchemaValidationContext(object o) { Push(o.GetType().Name); diff --git a/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/JsonArrayValidator.cs b/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/JsonArrayValidator.cs index b162c4354..65a7df587 100644 --- a/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/JsonArrayValidator.cs +++ b/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/JsonArrayValidator.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; @@ -145,10 +146,12 @@ namespace UniJSON } var count = GenericCounter.Count(o); - if (count == 0) + + // Empty array is valid + /*if (count == 0) { return new JsonSchemaValidationException(context, "empty"); - } + }*/ if (MaxItems.HasValue && count > MaxItems.Value) { @@ -160,6 +163,36 @@ namespace UniJSON return new JsonSchemaValidationException(context, "minItems"); } + if (Items == null) + { + return null; // There are no json schema for items, success + } + + var v = Items.Validator; + var t = o.GetType(); + IEnumerable iter = null; + if (t.IsArray) + { + iter = o as Array; + } + else if (t.GetIsGenericList()) + { + iter = o as IList; + } + else + { + return new JsonSchemaValidationException(context, "non iterable object"); + } + + foreach(var e in iter) + { + var ex = v.Validate(context, e); + if (ex != null) + { + return ex; + } + }; + return null; } @@ -252,7 +285,7 @@ namespace UniJSON } } - public void Deserialize(ListTreeNode src, ref U dst) + public void Deserialize(ListTreeNode src, ref U dst) where T : IListTreeItem, IValue { src.Deserialize(ref dst); diff --git a/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/JsonObjectValidator.cs b/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/JsonObjectValidator.cs index ae943ae65..631ae8563 100644 --- a/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/JsonObjectValidator.cs +++ b/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/JsonObjectValidator.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; - namespace UniJSON { /// @@ -28,11 +27,11 @@ namespace UniJSON get; set; } - List m_required = new List(); + HashSet m_required = new HashSet(); /// /// http://json-schema.org/latest/json-schema-validation.html#rfc.section.6.5.3 /// - public List Required + public HashSet Required { get { return m_required; } } @@ -302,18 +301,26 @@ namespace UniJSON { class ObjectValidator { - delegate JsonSchemaValidationException FieldValidator(IJsonSchemaValidator v, - JsonSchemaValidationContext c, T o); + delegate JsonSchemaValidationException FieldValidator( + JsonSchema s, JsonSchemaValidationContext c, T o, bool isRequired); Dictionary m_validators = new Dictionary(); static FieldValidator CreteFieldValidator(Func getter, string name) { - return (v, c, o) => + return (s, c, o, isRequired) => { + var v = s.Validator; using (c.Push(name)) { - return v.Validate(c, getter(o)); + var field = getter(o); + var ex = v.Validate(c, field); + if (ex != null && !isRequired && s.IsExplicitlyIgnorableValue(field)) + { + return null; + } + + return ex; } }; } @@ -341,32 +348,39 @@ namespace UniJSON } } - public JsonSchemaValidationException Validate(List required, Dictionary properties, + public JsonSchemaValidationException Validate( + HashSet required, + Dictionary properties, JsonSchemaValidationContext c, T o) { - foreach (var x in required) + foreach (var kv in properties) { - JsonSchema s; - if(properties.TryGetValue(x, out s)) + var fieldName = kv.Key; + var schema = kv.Value; + + FieldValidator fv; + if (m_validators.TryGetValue(fieldName, out fv)) { - FieldValidator fv; - if (m_validators.TryGetValue(x, out fv)) + var isRequired = required != null && required.Contains(fieldName); + var ex = fv(schema, c, o, isRequired); + if (ex != null) { - var ex = fv(s.Validator, c, o); - if (ex != null) + if (isRequired // required fields must be checked + || c.EnableDiagnosisForNotRequiredFields) { return ex; } } } } + return null; } } static ObjectValidator s_validator; - public static JsonSchemaValidationException Validate(List required, + public static JsonSchemaValidationException Validate(HashSet required, Dictionary properties, JsonSchemaValidationContext c, T o) { @@ -390,16 +404,7 @@ namespace UniJSON return new JsonSchemaValidationException(c, "no properties"); } - if (Required != null) - { - var ex = GenericValidator.Validate(Required, Properties, c, o); - if (ex != null) - { - return ex; - } - } - - return null; + return GenericValidator.Validate(Required, Properties, c, o); } static class GenericSerializer @@ -511,7 +516,7 @@ namespace UniJSON GenericSerializer.Serialize(this, f, c, value); } - static class GenericDeserializer + static class GenericDeserializer where S : IListTreeItem, IValue { delegate T Deserializer(ListTreeNode src); @@ -596,7 +601,7 @@ namespace UniJSON } } - public void Deserialize(ListTreeNode src, ref U dst) + public void Deserialize(ListTreeNode src, ref U dst) where T : IListTreeItem, IValue { GenericDeserializer.Deserialize(src, ref dst, Properties); diff --git a/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/JsonStringValidator.cs b/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/JsonStringValidator.cs index fb55cedb5..5b5f86d07 100644 --- a/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/JsonStringValidator.cs +++ b/Assets/VRM/UniJSON/Scripts/JsonSchemaValidator/JsonStringValidator.cs @@ -111,10 +111,6 @@ namespace UniJSON } var value = o as string; - if (value.All(x => Char.IsWhiteSpace(x))) - { - return new JsonSchemaValidationException(c, "whitespace"); - } if (MinLength.HasValue && value.Length < MinLength) {