├── .gitignore ├── .gitmodules ├── .travis.yml ├── KiteJSONValidator.podspec ├── KiteJSONValidator.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── KiteJSONResources.xcscheme │ └── KiteJSONValidatorTests.xcscheme ├── LICENSE ├── README.md ├── Resources ├── KiteJSONValidator-Info.plist ├── ReferenceSchemae │ └── schema └── en.lproj │ └── InfoPlist.strings ├── Sources ├── KiteJSONValidator.h ├── KiteJSONValidator.m ├── KiteValidationPair.h └── KiteValidationPair.m └── Tests ├── KiteJSONValidatorTests.m ├── Tests-Info.plist ├── Tests-Prefix.pch └── en.lproj └── InfoPlist.strings /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | */build/* 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | profile 14 | *.moved-aside 15 | DerivedData 16 | .idea/ 17 | *.hmap 18 | *.xccheckout 19 | 20 | #CocoaPods 21 | Pods 22 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Tests/JSON-Schema-Test-Suite"] 2 | path = Tests/JSON-Schema-Test-Suite 3 | url = https://github.com/json-schema/JSON-Schema-Test-Suite.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | before_script: 3 | - export LANG=en_US.UTF-8 4 | before_install: 5 | - brew update 6 | - brew uninstall xctool 7 | - brew install xctool 8 | script: 9 | - xctool -project KiteJSONValidator.xcodeproj -scheme KiteJSONValidatorTests -sdk iphonesimulator test ONLY_ACTIVE_ARCH=NO 10 | -------------------------------------------------------------------------------- /KiteJSONValidator.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "KiteJSONValidator" 3 | s.summary = "A native Objective-C JSON schema validator supporting JSON Schema draft 4" 4 | s.description = <<-DESC 5 | A native Objective-C JSON schema validator supporting [JSON Schema draft 4] [schemalink] released under the MIT license. 6 | 7 | Notes: This implementation does not support inline dereferencing (see [section 7.2.3] [section723] of the JSON Schema Spec). 8 | 9 | Development discussion [here] [devLink] 10 | 11 | [schemalink]: http://tools.ietf.org/html/draft-zyp-json-schema-04 12 | [section723]: http://json-schema.org/latest/json-schema-core.html#anchor30 13 | [devlink]: https://groups.google.com/forum/#!forum/kitejsonvalidator-development 14 | DESC 15 | s.homepage = "https://github.com/samskiter/KiteJSONValidator" 16 | s.license = "MIT" 17 | s.authors = { "Sam Duke" => "samskiter@users.noreply.github.com" } 18 | s.version = "0.2.2" 19 | s.source = { :git => "https://github.com/samskiter/KiteJSONValidator.git", :tag => "v#{s.version}"} 20 | s.platform = :ios, '7.0' 21 | s.requires_arc = true 22 | s.source_files = "Sources/*.{h,m}" 23 | s.xcconfig = { 24 | 'ONLY_ACTIVE_ARCH' => 'NO' 25 | } 26 | s.subspec "KiteJSONResources" do |sp| 27 | sp.resource_bundles = { 'KiteJSONValidator' => ['Resources/ReferenceSchemae/*'] } 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /KiteJSONValidator.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 37C8BA21191058D2004D3E23 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 37C8BA1F191058D2004D3E23 /* InfoPlist.strings */; }; 11 | 37C8BA2819105A17004D3E23 /* schema in Resources */ = {isa = PBXBuildFile; fileRef = 37C8BA2719105A17004D3E23 /* schema */; }; 12 | 37C8BA2919105A41004D3E23 /* KiteJSONValidator.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 37C8BA19191058D1004D3E23 /* KiteJSONValidator.bundle */; }; 13 | 67AE80EC18929CDB00620BF5 /* KiteValidationPair.m in Sources */ = {isa = PBXBuildFile; fileRef = 67AE80EB18929CDB00620BF5 /* KiteValidationPair.m */; }; 14 | 67B6B11E188C331200E1630A /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67B6B11D188C331200E1630A /* XCTest.framework */; }; 15 | 67B6B128188C331200E1630A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67B6B126188C331200E1630A /* InfoPlist.strings */; }; 16 | 67B6B12A188C331200E1630A /* KiteJSONValidatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 67B6B129188C331200E1630A /* KiteJSONValidatorTests.m */; }; 17 | 67B6B131188C639600E1630A /* JSON-Schema-Test-Suite in Resources */ = {isa = PBXBuildFile; fileRef = 67B6B130188C639600E1630A /* JSON-Schema-Test-Suite */; }; 18 | 67EDF5E7188C823F004DEC72 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67EDF5E6188C823F004DEC72 /* UIKit.framework */; }; 19 | 67EDF5E9188C8247004DEC72 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67EDF5E8188C8247004DEC72 /* Foundation.framework */; }; 20 | 67EDF5EC188C8373004DEC72 /* KiteJSONValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 67EDF5DE188C81CE004DEC72 /* KiteJSONValidator.m */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | 37C8BA2A19105A48004D3E23 /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = 67B6B10F188C32E800E1630A /* Project object */; 27 | proxyType = 1; 28 | remoteGlobalIDString = 37C8BA18191058D1004D3E23; 29 | remoteInfo = KiteJSONResources; 30 | }; 31 | /* End PBXContainerItemProxy section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 37C8BA19191058D1004D3E23 /* KiteJSONValidator.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KiteJSONValidator.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | 37C8BA1E191058D2004D3E23 /* KiteJSONValidator-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "KiteJSONValidator-Info.plist"; sourceTree = ""; }; 36 | 37C8BA20191058D2004D3E23 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 37 | 37C8BA2719105A17004D3E23 /* schema */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = schema; sourceTree = ""; }; 38 | 67AE80EA18929CDB00620BF5 /* KiteValidationPair.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KiteValidationPair.h; sourceTree = ""; }; 39 | 67AE80EB18929CDB00620BF5 /* KiteValidationPair.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KiteValidationPair.m; sourceTree = ""; }; 40 | 67B6B11A188C331200E1630A /* KiteJSONValidatorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KiteJSONValidatorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 67B6B11D188C331200E1630A /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 42 | 67B6B125188C331200E1630A /* Tests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = ""; }; 43 | 67B6B127188C331200E1630A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 44 | 67B6B129188C331200E1630A /* KiteJSONValidatorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KiteJSONValidatorTests.m; sourceTree = ""; }; 45 | 67B6B12B188C331200E1630A /* Tests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tests-Prefix.pch"; sourceTree = ""; }; 46 | 67B6B130188C639600E1630A /* JSON-Schema-Test-Suite */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "JSON-Schema-Test-Suite"; sourceTree = ""; }; 47 | 67EDF5DD188C81CE004DEC72 /* KiteJSONValidator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KiteJSONValidator.h; sourceTree = ""; }; 48 | 67EDF5DE188C81CE004DEC72 /* KiteJSONValidator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KiteJSONValidator.m; sourceTree = ""; }; 49 | 67EDF5E6188C823F004DEC72 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS7.0.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; 50 | 67EDF5E8188C8247004DEC72 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS7.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; 51 | /* End PBXFileReference section */ 52 | 53 | /* Begin PBXFrameworksBuildPhase section */ 54 | 37C8BA16191058D1004D3E23 /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | 67B6B117188C331200E1630A /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | 67EDF5E9188C8247004DEC72 /* Foundation.framework in Frameworks */, 66 | 67EDF5E7188C823F004DEC72 /* UIKit.framework in Frameworks */, 67 | 67B6B11E188C331200E1630A /* XCTest.framework in Frameworks */, 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | /* End PBXFrameworksBuildPhase section */ 72 | 73 | /* Begin PBXGroup section */ 74 | 37C8BA1C191058D2004D3E23 /* Resources */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 37C8BA2619105A17004D3E23 /* ReferenceSchemae */, 78 | 37C8BA1D191058D2004D3E23 /* Supporting Files */, 79 | ); 80 | path = Resources; 81 | sourceTree = ""; 82 | }; 83 | 37C8BA1D191058D2004D3E23 /* Supporting Files */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 37C8BA1E191058D2004D3E23 /* KiteJSONValidator-Info.plist */, 87 | 37C8BA1F191058D2004D3E23 /* InfoPlist.strings */, 88 | ); 89 | name = "Supporting Files"; 90 | sourceTree = ""; 91 | }; 92 | 37C8BA2619105A17004D3E23 /* ReferenceSchemae */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 37C8BA2719105A17004D3E23 /* schema */, 96 | ); 97 | path = ReferenceSchemae; 98 | sourceTree = ""; 99 | }; 100 | 67B6B10E188C32E800E1630A = { 101 | isa = PBXGroup; 102 | children = ( 103 | 67EDF5E1188C81CE004DEC72 /* KiteJSONValidator */, 104 | 37C8BA1C191058D2004D3E23 /* Resources */, 105 | 67B6B123188C331200E1630A /* Tests */, 106 | 67B6B11C188C331200E1630A /* Frameworks */, 107 | 67B6B11B188C331200E1630A /* Products */, 108 | ); 109 | sourceTree = ""; 110 | }; 111 | 67B6B11B188C331200E1630A /* Products */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 67B6B11A188C331200E1630A /* KiteJSONValidatorTests.xctest */, 115 | 37C8BA19191058D1004D3E23 /* KiteJSONValidator.bundle */, 116 | ); 117 | name = Products; 118 | sourceTree = ""; 119 | }; 120 | 67B6B11C188C331200E1630A /* Frameworks */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | 67EDF5E8188C8247004DEC72 /* Foundation.framework */, 124 | 67EDF5E6188C823F004DEC72 /* UIKit.framework */, 125 | 67B6B11D188C331200E1630A /* XCTest.framework */, 126 | ); 127 | name = Frameworks; 128 | sourceTree = ""; 129 | }; 130 | 67B6B123188C331200E1630A /* Tests */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 67B6B129188C331200E1630A /* KiteJSONValidatorTests.m */, 134 | 67B6B130188C639600E1630A /* JSON-Schema-Test-Suite */, 135 | 67B6B124188C331200E1630A /* Supporting Files */, 136 | ); 137 | path = Tests; 138 | sourceTree = ""; 139 | }; 140 | 67B6B124188C331200E1630A /* Supporting Files */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 67B6B125188C331200E1630A /* Tests-Info.plist */, 144 | 67B6B126188C331200E1630A /* InfoPlist.strings */, 145 | 67B6B12B188C331200E1630A /* Tests-Prefix.pch */, 146 | ); 147 | name = "Supporting Files"; 148 | sourceTree = ""; 149 | }; 150 | 67EDF5E1188C81CE004DEC72 /* KiteJSONValidator */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 67EDF5DD188C81CE004DEC72 /* KiteJSONValidator.h */, 154 | 67EDF5DE188C81CE004DEC72 /* KiteJSONValidator.m */, 155 | 67AE80EA18929CDB00620BF5 /* KiteValidationPair.h */, 156 | 67AE80EB18929CDB00620BF5 /* KiteValidationPair.m */, 157 | ); 158 | name = KiteJSONValidator; 159 | path = Sources; 160 | sourceTree = ""; 161 | }; 162 | /* End PBXGroup section */ 163 | 164 | /* Begin PBXNativeTarget section */ 165 | 37C8BA18191058D1004D3E23 /* KiteJSONResources */ = { 166 | isa = PBXNativeTarget; 167 | buildConfigurationList = 37C8BA23191058D2004D3E23 /* Build configuration list for PBXNativeTarget "KiteJSONResources" */; 168 | buildPhases = ( 169 | 37C8BA15191058D1004D3E23 /* Sources */, 170 | 37C8BA16191058D1004D3E23 /* Frameworks */, 171 | 37C8BA17191058D1004D3E23 /* Resources */, 172 | ); 173 | buildRules = ( 174 | ); 175 | dependencies = ( 176 | ); 177 | name = KiteJSONResources; 178 | productName = KiteJSONValidator; 179 | productReference = 37C8BA19191058D1004D3E23 /* KiteJSONValidator.bundle */; 180 | productType = "com.apple.product-type.bundle"; 181 | }; 182 | 67B6B119188C331200E1630A /* KiteJSONValidatorTests */ = { 183 | isa = PBXNativeTarget; 184 | buildConfigurationList = 67B6B12C188C331200E1630A /* Build configuration list for PBXNativeTarget "KiteJSONValidatorTests" */; 185 | buildPhases = ( 186 | 67B6B116188C331200E1630A /* Sources */, 187 | 67B6B117188C331200E1630A /* Frameworks */, 188 | 67B6B118188C331200E1630A /* Resources */, 189 | ); 190 | buildRules = ( 191 | ); 192 | dependencies = ( 193 | 37C8BA2B19105A48004D3E23 /* PBXTargetDependency */, 194 | ); 195 | name = KiteJSONValidatorTests; 196 | productName = Tests; 197 | productReference = 67B6B11A188C331200E1630A /* KiteJSONValidatorTests.xctest */; 198 | productType = "com.apple.product-type.bundle.unit-test"; 199 | }; 200 | /* End PBXNativeTarget section */ 201 | 202 | /* Begin PBXProject section */ 203 | 67B6B10F188C32E800E1630A /* Project object */ = { 204 | isa = PBXProject; 205 | attributes = { 206 | CLASSPREFIX = Kite; 207 | LastUpgradeCheck = 0610; 208 | ORGANIZATIONNAME = KiteJSONValidator; 209 | }; 210 | buildConfigurationList = 67B6B112188C32E800E1630A /* Build configuration list for PBXProject "KiteJSONValidator" */; 211 | compatibilityVersion = "Xcode 3.2"; 212 | developmentRegion = English; 213 | hasScannedForEncodings = 0; 214 | knownRegions = ( 215 | en, 216 | ); 217 | mainGroup = 67B6B10E188C32E800E1630A; 218 | productRefGroup = 67B6B11B188C331200E1630A /* Products */; 219 | projectDirPath = ""; 220 | projectRoot = ""; 221 | targets = ( 222 | 67B6B119188C331200E1630A /* KiteJSONValidatorTests */, 223 | 37C8BA18191058D1004D3E23 /* KiteJSONResources */, 224 | ); 225 | }; 226 | /* End PBXProject section */ 227 | 228 | /* Begin PBXResourcesBuildPhase section */ 229 | 37C8BA17191058D1004D3E23 /* Resources */ = { 230 | isa = PBXResourcesBuildPhase; 231 | buildActionMask = 2147483647; 232 | files = ( 233 | 37C8BA21191058D2004D3E23 /* InfoPlist.strings in Resources */, 234 | 37C8BA2819105A17004D3E23 /* schema in Resources */, 235 | ); 236 | runOnlyForDeploymentPostprocessing = 0; 237 | }; 238 | 67B6B118188C331200E1630A /* Resources */ = { 239 | isa = PBXResourcesBuildPhase; 240 | buildActionMask = 2147483647; 241 | files = ( 242 | 37C8BA2919105A41004D3E23 /* KiteJSONValidator.bundle in Resources */, 243 | 67B6B131188C639600E1630A /* JSON-Schema-Test-Suite in Resources */, 244 | 67B6B128188C331200E1630A /* InfoPlist.strings in Resources */, 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | }; 248 | /* End PBXResourcesBuildPhase section */ 249 | 250 | /* Begin PBXSourcesBuildPhase section */ 251 | 37C8BA15191058D1004D3E23 /* Sources */ = { 252 | isa = PBXSourcesBuildPhase; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | ); 256 | runOnlyForDeploymentPostprocessing = 0; 257 | }; 258 | 67B6B116188C331200E1630A /* Sources */ = { 259 | isa = PBXSourcesBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | 67EDF5EC188C8373004DEC72 /* KiteJSONValidator.m in Sources */, 263 | 67AE80EC18929CDB00620BF5 /* KiteValidationPair.m in Sources */, 264 | 67B6B12A188C331200E1630A /* KiteJSONValidatorTests.m in Sources */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | /* End PBXSourcesBuildPhase section */ 269 | 270 | /* Begin PBXTargetDependency section */ 271 | 37C8BA2B19105A48004D3E23 /* PBXTargetDependency */ = { 272 | isa = PBXTargetDependency; 273 | target = 37C8BA18191058D1004D3E23 /* KiteJSONResources */; 274 | targetProxy = 37C8BA2A19105A48004D3E23 /* PBXContainerItemProxy */; 275 | }; 276 | /* End PBXTargetDependency section */ 277 | 278 | /* Begin PBXVariantGroup section */ 279 | 37C8BA1F191058D2004D3E23 /* InfoPlist.strings */ = { 280 | isa = PBXVariantGroup; 281 | children = ( 282 | 37C8BA20191058D2004D3E23 /* en */, 283 | ); 284 | name = InfoPlist.strings; 285 | sourceTree = ""; 286 | }; 287 | 67B6B126188C331200E1630A /* InfoPlist.strings */ = { 288 | isa = PBXVariantGroup; 289 | children = ( 290 | 67B6B127188C331200E1630A /* en */, 291 | ); 292 | name = InfoPlist.strings; 293 | sourceTree = ""; 294 | }; 295 | /* End PBXVariantGroup section */ 296 | 297 | /* Begin XCBuildConfiguration section */ 298 | 37C8BA24191058D2004D3E23 /* Debug */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ALWAYS_SEARCH_USER_PATHS = NO; 302 | COMBINE_HIDPI_IMAGES = YES; 303 | INFOPLIST_FILE = "Resources/KiteJSONValidator-Info.plist"; 304 | PRODUCT_NAME = KiteJSONValidator; 305 | SKIP_INSTALL = YES; 306 | WRAPPER_EXTENSION = bundle; 307 | }; 308 | name = Debug; 309 | }; 310 | 37C8BA25191058D2004D3E23 /* Release */ = { 311 | isa = XCBuildConfiguration; 312 | buildSettings = { 313 | ALWAYS_SEARCH_USER_PATHS = NO; 314 | COMBINE_HIDPI_IMAGES = YES; 315 | INFOPLIST_FILE = "Resources/KiteJSONValidator-Info.plist"; 316 | PRODUCT_NAME = KiteJSONValidator; 317 | SKIP_INSTALL = YES; 318 | WRAPPER_EXTENSION = bundle; 319 | }; 320 | name = Release; 321 | }; 322 | 67B6B113188C32E800E1630A /* Debug */ = { 323 | isa = XCBuildConfiguration; 324 | buildSettings = { 325 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 326 | CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; 327 | CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES; 328 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 329 | GCC_WARN_MULTIPLE_DEFINITION_TYPES_FOR_SELECTOR = YES; 330 | GCC_WARN_STRICT_SELECTOR_MATCH = YES; 331 | GCC_WARN_UNDECLARED_SELECTOR = YES; 332 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 333 | ONLY_ACTIVE_ARCH = YES; 334 | SDKROOT = iphoneos; 335 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; 336 | }; 337 | name = Debug; 338 | }; 339 | 67B6B114188C32E800E1630A /* Release */ = { 340 | isa = XCBuildConfiguration; 341 | buildSettings = { 342 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 343 | CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; 344 | CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES; 345 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 346 | GCC_WARN_MULTIPLE_DEFINITION_TYPES_FOR_SELECTOR = YES; 347 | GCC_WARN_STRICT_SELECTOR_MATCH = YES; 348 | GCC_WARN_UNDECLARED_SELECTOR = YES; 349 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 350 | SDKROOT = iphoneos; 351 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; 352 | }; 353 | name = Release; 354 | }; 355 | 67B6B12D188C331200E1630A /* Debug */ = { 356 | isa = XCBuildConfiguration; 357 | buildSettings = { 358 | ALWAYS_SEARCH_USER_PATHS = NO; 359 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 360 | CLANG_CXX_LIBRARY = "libc++"; 361 | CLANG_ENABLE_MODULES = YES; 362 | CLANG_ENABLE_OBJC_ARC = YES; 363 | CLANG_WARN_BOOL_CONVERSION = YES; 364 | CLANG_WARN_CONSTANT_CONVERSION = YES; 365 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 366 | CLANG_WARN_EMPTY_BODY = YES; 367 | CLANG_WARN_ENUM_CONVERSION = YES; 368 | CLANG_WARN_INT_CONVERSION = YES; 369 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 370 | CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; 371 | CLANG_WARN_UNREACHABLE_CODE = YES; 372 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 373 | COPY_PHASE_STRIP = NO; 374 | FRAMEWORK_SEARCH_PATHS = ( 375 | "$(SDKROOT)/Developer/Library/Frameworks", 376 | "$(inherited)", 377 | "$(DEVELOPER_FRAMEWORKS_DIR)", 378 | ); 379 | GCC_C_LANGUAGE_STANDARD = gnu11; 380 | GCC_DYNAMIC_NO_PIC = NO; 381 | GCC_OPTIMIZATION_LEVEL = 0; 382 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 383 | GCC_PREFIX_HEADER = "Tests/Tests-Prefix.pch"; 384 | GCC_PREPROCESSOR_DEFINITIONS = ( 385 | "DEBUG=1", 386 | "$(inherited)", 387 | ); 388 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 389 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 390 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 391 | GCC_WARN_ABOUT_MISSING_NEWLINE = YES; 392 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 393 | GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; 394 | GCC_WARN_SHADOW = YES; 395 | GCC_WARN_SIGN_COMPARE = YES; 396 | GCC_WARN_UNDECLARED_SELECTOR = YES; 397 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 398 | GCC_WARN_UNKNOWN_PRAGMAS = YES; 399 | GCC_WARN_UNUSED_FUNCTION = YES; 400 | GCC_WARN_UNUSED_LABEL = YES; 401 | GCC_WARN_UNUSED_VARIABLE = YES; 402 | INFOPLIST_FILE = "Tests/Tests-Info.plist"; 403 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 404 | PRODUCT_NAME = "$(TARGET_NAME)"; 405 | WRAPPER_EXTENSION = xctest; 406 | }; 407 | name = Debug; 408 | }; 409 | 67B6B12E188C331200E1630A /* Release */ = { 410 | isa = XCBuildConfiguration; 411 | buildSettings = { 412 | ALWAYS_SEARCH_USER_PATHS = NO; 413 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 414 | CLANG_CXX_LIBRARY = "libc++"; 415 | CLANG_ENABLE_MODULES = YES; 416 | CLANG_ENABLE_OBJC_ARC = YES; 417 | CLANG_WARN_BOOL_CONVERSION = YES; 418 | CLANG_WARN_CONSTANT_CONVERSION = YES; 419 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 420 | CLANG_WARN_EMPTY_BODY = YES; 421 | CLANG_WARN_ENUM_CONVERSION = YES; 422 | CLANG_WARN_INT_CONVERSION = YES; 423 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 424 | CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; 425 | CLANG_WARN_UNREACHABLE_CODE = YES; 426 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 427 | COPY_PHASE_STRIP = YES; 428 | ENABLE_NS_ASSERTIONS = NO; 429 | FRAMEWORK_SEARCH_PATHS = ( 430 | "$(SDKROOT)/Developer/Library/Frameworks", 431 | "$(inherited)", 432 | "$(DEVELOPER_FRAMEWORKS_DIR)", 433 | ); 434 | GCC_C_LANGUAGE_STANDARD = gnu11; 435 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 436 | GCC_PREFIX_HEADER = "Tests/Tests-Prefix.pch"; 437 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 438 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 439 | GCC_WARN_ABOUT_MISSING_NEWLINE = YES; 440 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 441 | GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; 442 | GCC_WARN_SHADOW = YES; 443 | GCC_WARN_SIGN_COMPARE = YES; 444 | GCC_WARN_UNDECLARED_SELECTOR = YES; 445 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 446 | GCC_WARN_UNKNOWN_PRAGMAS = YES; 447 | GCC_WARN_UNUSED_FUNCTION = YES; 448 | GCC_WARN_UNUSED_LABEL = YES; 449 | GCC_WARN_UNUSED_VARIABLE = YES; 450 | INFOPLIST_FILE = "Tests/Tests-Info.plist"; 451 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 452 | PRODUCT_NAME = "$(TARGET_NAME)"; 453 | VALIDATE_PRODUCT = YES; 454 | WRAPPER_EXTENSION = xctest; 455 | }; 456 | name = Release; 457 | }; 458 | /* End XCBuildConfiguration section */ 459 | 460 | /* Begin XCConfigurationList section */ 461 | 37C8BA23191058D2004D3E23 /* Build configuration list for PBXNativeTarget "KiteJSONResources" */ = { 462 | isa = XCConfigurationList; 463 | buildConfigurations = ( 464 | 37C8BA24191058D2004D3E23 /* Debug */, 465 | 37C8BA25191058D2004D3E23 /* Release */, 466 | ); 467 | defaultConfigurationIsVisible = 0; 468 | defaultConfigurationName = Release; 469 | }; 470 | 67B6B112188C32E800E1630A /* Build configuration list for PBXProject "KiteJSONValidator" */ = { 471 | isa = XCConfigurationList; 472 | buildConfigurations = ( 473 | 67B6B113188C32E800E1630A /* Debug */, 474 | 67B6B114188C32E800E1630A /* Release */, 475 | ); 476 | defaultConfigurationIsVisible = 0; 477 | defaultConfigurationName = Release; 478 | }; 479 | 67B6B12C188C331200E1630A /* Build configuration list for PBXNativeTarget "KiteJSONValidatorTests" */ = { 480 | isa = XCConfigurationList; 481 | buildConfigurations = ( 482 | 67B6B12D188C331200E1630A /* Debug */, 483 | 67B6B12E188C331200E1630A /* Release */, 484 | ); 485 | defaultConfigurationIsVisible = 0; 486 | defaultConfigurationName = Release; 487 | }; 488 | /* End XCConfigurationList section */ 489 | }; 490 | rootObject = 67B6B10F188C32E800E1630A /* Project object */; 491 | } 492 | -------------------------------------------------------------------------------- /KiteJSONValidator.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /KiteJSONValidator.xcodeproj/xcshareddata/xcschemes/KiteJSONResources.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 63 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /KiteJSONValidator.xcodeproj/xcshareddata/xcschemes/KiteJSONValidatorTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 66 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Sam Duke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | KiteJSONValidator 2 | ================= 3 | 4 | Master: [![Build Status](https://travis-ci.org/samskiter/KiteJSONValidator.svg?branch=master)](https://travis-ci.org/samskiter/KiteJSONValidator) 5 | 6 | Develop: [![Build Status](https://travis-ci.org/samskiter/KiteJSONValidator.svg?branch=develop)](https://travis-ci.org/samskiter/KiteJSONValidator) 7 | 8 | A native Objective-C JSON schema validator supporting [JSON Schema draft 4] [schemalink] released under the MIT license. 9 | 10 | Notes: This implementation does not support inline dereferencing (see [section 7.2.3] [section723] of the JSON Schema Spec). 11 | 12 | Development discussion [here] [devLink] 13 | 14 | [schemalink]: http://tools.ietf.org/html/draft-zyp-json-schema-04 15 | [section723]: http://json-schema.org/latest/json-schema-core.html#anchor30 16 | [devlink]: https://groups.google.com/forum/#!forum/kitejsonvalidator-development 17 | -------------------------------------------------------------------------------- /Resources/KiteJSONValidator-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | com.kitejsonvalidator.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | BNDL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | CFPlugInDynamicRegisterFunction 26 | 27 | CFPlugInDynamicRegistration 28 | NO 29 | CFPlugInFactories 30 | 31 | 00000000-0000-0000-0000-000000000000 32 | MyFactoryFunction 33 | 34 | CFPlugInTypes 35 | 36 | 00000000-0000-0000-0000-000000000000 37 | 38 | 00000000-0000-0000-0000-000000000000 39 | 40 | 41 | CFPlugInUnloadFunction 42 | 43 | NSHumanReadableCopyright 44 | Copyright © 2014 KiteJSONValidator. All rights reserved. 45 | 46 | 47 | -------------------------------------------------------------------------------- /Resources/ReferenceSchemae/schema: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://json-schema.org/draft-04/schema#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "description": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "positiveInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "positiveIntegerDefault0": { 16 | "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] 17 | }, 18 | "simpleTypes": { 19 | "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] 20 | }, 21 | "stringArray": { 22 | "type": "array", 23 | "items": { "type": "string" }, 24 | "minItems": 1, 25 | "uniqueItems": true 26 | } 27 | }, 28 | "type": "object", 29 | "properties": { 30 | "id": { 31 | "type": "string", 32 | "format": "uri" 33 | }, 34 | "$schema": { 35 | "type": "string", 36 | "format": "uri" 37 | }, 38 | "title": { 39 | "type": "string" 40 | }, 41 | "description": { 42 | "type": "string" 43 | }, 44 | "default": {}, 45 | "multipleOf": { 46 | "type": "number", 47 | "minimum": 0, 48 | "exclusiveMinimum": true 49 | }, 50 | "maximum": { 51 | "type": "number" 52 | }, 53 | "exclusiveMaximum": { 54 | "type": "boolean", 55 | "default": false 56 | }, 57 | "minimum": { 58 | "type": "number" 59 | }, 60 | "exclusiveMinimum": { 61 | "type": "boolean", 62 | "default": false 63 | }, 64 | "maxLength": { "$ref": "#/definitions/positiveInteger" }, 65 | "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, 66 | "pattern": { 67 | "type": "string", 68 | "format": "regex" 69 | }, 70 | "additionalItems": { 71 | "anyOf": [ 72 | { "type": "boolean" }, 73 | { "$ref": "#" } 74 | ], 75 | "default": {} 76 | }, 77 | "items": { 78 | "anyOf": [ 79 | { "$ref": "#" }, 80 | { "$ref": "#/definitions/schemaArray" } 81 | ], 82 | "default": {} 83 | }, 84 | "maxItems": { "$ref": "#/definitions/positiveInteger" }, 85 | "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, 86 | "uniqueItems": { 87 | "type": "boolean", 88 | "default": false 89 | }, 90 | "maxProperties": { "$ref": "#/definitions/positiveInteger" }, 91 | "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, 92 | "required": { "$ref": "#/definitions/stringArray" }, 93 | "additionalProperties": { 94 | "anyOf": [ 95 | { "type": "boolean" }, 96 | { "$ref": "#" } 97 | ], 98 | "default": {} 99 | }, 100 | "definitions": { 101 | "type": "object", 102 | "additionalProperties": { "$ref": "#" }, 103 | "default": {} 104 | }, 105 | "properties": { 106 | "type": "object", 107 | "additionalProperties": { "$ref": "#" }, 108 | "default": {} 109 | }, 110 | "patternProperties": { 111 | "type": "object", 112 | "additionalProperties": { "$ref": "#" }, 113 | "default": {} 114 | }, 115 | "dependencies": { 116 | "type": "object", 117 | "additionalProperties": { 118 | "anyOf": [ 119 | { "$ref": "#" }, 120 | { "$ref": "#/definitions/stringArray" } 121 | ] 122 | } 123 | }, 124 | "enum": { 125 | "type": "array", 126 | "minItems": 1, 127 | "uniqueItems": true 128 | }, 129 | "type": { 130 | "anyOf": [ 131 | { "$ref": "#/definitions/simpleTypes" }, 132 | { 133 | "type": "array", 134 | "items": { "$ref": "#/definitions/simpleTypes" }, 135 | "minItems": 1, 136 | "uniqueItems": true 137 | } 138 | ] 139 | }, 140 | "allOf": { "$ref": "#/definitions/schemaArray" }, 141 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 142 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 143 | "not": { "$ref": "#" } 144 | }, 145 | "dependencies": { 146 | "exclusiveMaximum": [ "maximum" ], 147 | "exclusiveMinimum": [ "minimum" ] 148 | }, 149 | "default": {} 150 | } 151 | -------------------------------------------------------------------------------- /Resources/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Sources/KiteJSONValidator.h: -------------------------------------------------------------------------------- 1 | // 2 | // KiteJSONValidator.h 3 | // MCode 4 | // 5 | // Created by Sam Duke on 15/12/2013. 6 | // Copyright (c) 2013 Airsource Ltd. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @protocol KiteJSONSchemaRefDelegate; 12 | 13 | @interface KiteJSONValidator : NSObject 14 | 15 | @property (nonatomic, weak) id delegate; 16 | 17 | /** 18 | Validates json against a draft4 schema. 19 | @see http://tools.ietf.org/html/draft-zyp-json-schema-04 20 | 21 | @param jsonData The JSON to be validated 22 | @param schemaData The draft4 JSON schema to validate against 23 | @return Whether the json is validated. 24 | */ 25 | -(BOOL)validateJSONData:(NSData*)jsonData withSchemaData:(NSData*)schemaData; 26 | -(BOOL)validateJSONInstance:(id)json withSchema:(NSDictionary*)schema; 27 | -(BOOL)validateJSONInstance:(id)json withSchemaData:(NSData*)schemaData; 28 | //TODO:add an interface to add a schema with a key, allowing a schema to only be validated once and then reused 29 | 30 | /** 31 | Used for adding an ENTIRE document to the list of reference schemas - the URL should therefore be fragmentless. 32 | 33 | @param schemaData The data for the document to be converted to JSON 34 | @param url The fragmentless URL for this document 35 | 36 | @return Whether the reference schema was successfully added. 37 | */ 38 | -(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url; 39 | 40 | /** 41 | Used for adding an ENTIRE document to the list of reference schemas - the URL should therefore be fragmentless. 42 | 43 | @param schemaData The data for the document to be converted to JSON 44 | @param url The fragmentless URL for this document 45 | @param shouldValidateSchema Whether the new reference schema should be validated against the "root" schema. 46 | 47 | @return Whether the reference schema was successfully added. 48 | */ 49 | -(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BOOL)shouldValidateSchema; 50 | 51 | /** 52 | Used for adding an ENTIRE document to the list of reference schemas - the URL should therefore be fragmentless. 53 | 54 | @param schema The dictionary representation of the JSON schema (the JSON was therefore valid). 55 | @param url The fragmentless URL for this document 56 | 57 | @return Whether the reference schema was successfully added. 58 | */ 59 | -(BOOL)addRefSchema:(NSDictionary*)schema atURL:(NSURL*)url; 60 | 61 | /** 62 | Used for adding an ENTIRE document to the list of reference schemas - the URL should therefore be fragmentless. 63 | 64 | @param schema The dictionary representation of the JSON schema (the JSON was therefore valid). 65 | @param url The fragmentless URL for this document 66 | @param shouldValidateSchema Whether the new reference schema should be validated against the "root" schema. 67 | 68 | @return Whether the reference schema was successfully added. 69 | */ 70 | -(BOOL)addRefSchema:(NSDictionary *)schema atURL:(NSURL *)url validateSchema:(BOOL)shouldValidateSchema; 71 | 72 | @end 73 | 74 | @protocol KiteJSONSchemaRefDelegate 75 | 76 | -(NSData*)schemaValidator:(KiteJSONValidator*)validator requiresSchemaDataForRefURL:(NSURL*)refURL; 77 | -(NSDictionary*)schemaValidator:(KiteJSONValidator*)validator requiresSchemaForRefURL:(NSURL*)refURL; 78 | 79 | @end 80 | -------------------------------------------------------------------------------- /Sources/KiteJSONValidator.m: -------------------------------------------------------------------------------- 1 | // 2 | // KiteJSONValidator.m 3 | // MCode 4 | // 5 | // Created by Sam Duke on 15/12/2013. 6 | // Copyright (c) 2013 Airsource Ltd. All rights reserved. 7 | // 8 | 9 | #import "KiteJSONValidator.h" 10 | #import "KiteValidationPair.h" 11 | 12 | @interface KiteJSONValidator() 13 | 14 | @property (nonatomic,strong) NSMutableArray * validationStack; 15 | @property (nonatomic,strong) NSMutableArray * resolutionStack; 16 | @property (nonatomic,strong) NSMutableArray * schemaStack; 17 | @property (nonatomic,strong) NSMutableDictionary * schemaRefs; 18 | 19 | @end 20 | 21 | @implementation KiteJSONValidator 22 | 23 | @synthesize validationStack=_validationStack; 24 | @synthesize resolutionStack=_resolutionStack; 25 | @synthesize schemaStack=_schemaStack; 26 | @synthesize schemaRefs=_schemaRefs; 27 | 28 | @synthesize delegate; 29 | 30 | -(id)init 31 | { 32 | self = [super init]; 33 | if (self) { 34 | NSURL *rootURL = [NSURL URLWithString:@"http://json-schema.org/draft-04/schema#"]; 35 | NSDictionary *rootSchema = [self rootSchema]; 36 | #pragma clang diagnostic push 37 | #pragma clang diagnostic ignored "-Wunused-variable" 38 | BOOL success = [self addRefSchema:rootSchema atURL:rootURL validateSchema:NO]; 39 | #pragma clang diagnostic pop 40 | NSAssert(success == YES, @"Unable to add the root schema!", nil); 41 | } 42 | 43 | return self; 44 | } 45 | 46 | -(BOOL)addRefSchema:(NSDictionary *)schema atURL:(NSURL *)url validateSchema:(BOOL)shouldValidateSchema 47 | { 48 | NSError * error; 49 | //We convert to data in order to protect ourselves against a cyclic structure and ensure we have valid JSON 50 | NSData * schemaData = [NSJSONSerialization dataWithJSONObject:schema options:0 error:&error]; 51 | if (error != nil) { 52 | return NO; 53 | } 54 | return [self addRefSchemaData:schemaData atURL:url validateSchema:shouldValidateSchema]; 55 | } 56 | 57 | -(BOOL)addRefSchema:(NSDictionary*)schema atURL:(NSURL*)url 58 | { 59 | return [self addRefSchema:schema atURL:url validateSchema:YES]; 60 | } 61 | 62 | -(BOOL)addRefSchemaData:(NSData *)schemaData atURL:(NSURL *)url 63 | { 64 | return [self addRefSchemaData:schemaData atURL:url validateSchema:YES]; 65 | } 66 | 67 | -(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BOOL)shouldValidateSchema 68 | { 69 | if (!schemaData || ![schemaData isKindOfClass:[NSData class]]) { 70 | return NO; 71 | } 72 | 73 | NSError * error = nil; 74 | id schema = [NSJSONSerialization JSONObjectWithData:schemaData options:0 error:&error]; 75 | if (error != nil) { 76 | return NO; 77 | } else if (![schema isKindOfClass:[NSDictionary class]]) { 78 | return NO; 79 | } 80 | 81 | NSAssert(url != NULL, @"URL must not be empty", nil); 82 | NSAssert(schema != NULL, @"Schema must not be empty", nil); 83 | 84 | if (!url || !schema) 85 | { 86 | //NSLog(@"Invalid schema for URL (%@): %@", url, schema); 87 | return NO; 88 | } 89 | url = [self urlWithoutFragment:url]; 90 | //TODO:consider failing if the url contained a fragment. 91 | 92 | if (shouldValidateSchema) 93 | { 94 | NSDictionary *root = [self rootSchema]; 95 | if (![root isEqualToDictionary:schema]) 96 | { 97 | BOOL isValidSchema = [self validateJSON:schema withSchemaDict:root]; 98 | NSAssert(isValidSchema == YES, @"Invalid schema", nil); 99 | if (!isValidSchema) return NO; 100 | } 101 | else 102 | { 103 | //NSLog(@"Can't really validate the root schema against itself, right? ... Right?"); 104 | } 105 | } 106 | 107 | @synchronized(self) 108 | { 109 | if (!_schemaRefs) 110 | { 111 | _schemaRefs = [[NSMutableDictionary alloc] init]; 112 | } 113 | self.schemaRefs[url] = schema; 114 | return YES; 115 | } 116 | } 117 | 118 | -(NSDictionary *)rootSchema 119 | { 120 | static NSDictionary * rootSchema; 121 | static dispatch_once_t onceToken; 122 | dispatch_once(&onceToken, ^{ 123 | NSBundle *mainBundle = [NSBundle bundleForClass:[self class]]; 124 | NSString *bundlePath = [mainBundle pathForResource:@"KiteJSONValidator" ofType:@"bundle"]; 125 | NSBundle *resourceBundle = [NSBundle bundleWithPath:bundlePath]; 126 | NSString *rootSchemaPath = [resourceBundle pathForResource:@"schema" ofType:@""]; 127 | NSAssert(rootSchemaPath != NULL, @"Root schema not found in bundle: %@", resourceBundle.bundlePath); 128 | 129 | NSData *rootSchemaData = [NSData dataWithContentsOfFile:rootSchemaPath]; 130 | NSError *error = nil; 131 | rootSchema = [NSJSONSerialization JSONObjectWithData:rootSchemaData 132 | options:kNilOptions 133 | error:&error]; 134 | NSAssert(rootSchema != NULL, @"Root schema wasn't found", nil); 135 | NSAssert([rootSchema isKindOfClass:[NSDictionary class]], @"Root schema wasn't a dictionary", nil); 136 | }); 137 | 138 | return rootSchema; 139 | } 140 | 141 | -(BOOL)pushToStackJSON:(id)json forSchema:(NSDictionary*)schema 142 | { 143 | if (self.validationStack == nil) { 144 | self.validationStack = [NSMutableArray new]; 145 | self.resolutionStack = [NSMutableArray new]; 146 | self.schemaStack = [NSMutableArray new]; 147 | } 148 | KiteValidationPair * pair = [KiteValidationPair pairWithLeft:json right:schema]; 149 | if ([self.validationStack containsObject:pair]) { 150 | return NO; //Detects loops 151 | } 152 | [self.validationStack addObject:pair]; 153 | return YES; 154 | } 155 | 156 | -(void)popStack 157 | { 158 | [self.validationStack removeLastObject]; 159 | } 160 | 161 | -(NSURL*)urlWithoutFragment:(NSURL*)url 162 | { 163 | if (!url || ![url isKindOfClass:[NSURL class]]) { 164 | return nil; 165 | } 166 | 167 | NSString * refString = url.absoluteString; 168 | if (url.fragment.length > 0) { 169 | refString = [refString stringByReplacingOccurrencesOfString:url.fragment 170 | withString:@"" 171 | options:NSBackwardsSearch 172 | range:NSMakeRange(0, refString.length)]; 173 | } 174 | if ([refString hasSuffix:@"#"]) { 175 | refString = [refString substringToIndex:[refString length] - 1]; 176 | } 177 | return [NSURL URLWithString:refString]; 178 | } 179 | 180 | -(BOOL)validateJSON:(id)json withSchemaAtReference:(NSString*)refString 181 | { 182 | NSURL * refURI = [NSURL URLWithString:refString relativeToURL:self.resolutionStack.lastObject]; 183 | if (!refURI) 184 | { 185 | return NO; 186 | } 187 | 188 | //get the fragment, if it is a JSON-Pointer 189 | NSArray * pointerComponents = nil; 190 | if (refURI.fragment.length > 0 && [refURI.fragment hasPrefix:@"/"]) { 191 | NSURL * pointerURI = [NSURL URLWithString:refURI.fragment]; 192 | pointerComponents = [pointerURI pathComponents]; 193 | } 194 | refURI = [self urlWithoutFragment:refURI]; 195 | 196 | //first get the document, then resolve any pointers. 197 | NSURL * lastResolution = self.resolutionStack.lastObject; 198 | BOOL newDocument = NO; 199 | id schema = nil; 200 | 201 | if ([lastResolution isEqual:refURI]) { 202 | schema = (NSDictionary*)self.schemaStack.lastObject; 203 | } else if (self.schemaRefs != nil && self.schemaRefs[refURI] != nil) { 204 | //we changed document 205 | schema = self.schemaRefs[refURI]; 206 | [self setResolutionUrl:refURI forSchema:schema]; 207 | newDocument = YES; 208 | } 209 | 210 | if (!schema) { 211 | return NO; 212 | } 213 | 214 | for (NSString * component in pointerComponents) { 215 | if ([component isEqualToString:@"/"]) { 216 | continue; 217 | } 218 | 219 | if ([schema isKindOfClass:[NSDictionary class]]) { 220 | schema = ((NSDictionary *)schema)[component]; 221 | } else if ([schema isKindOfClass:[NSArray class]] && 222 | (NSInteger)[(NSArray*)schema count] > [component integerValue]) { 223 | if (component.floatValue == (float)component.integerValue) { 224 | schema = ((NSArray *)schema)[[component integerValue]]; 225 | } 226 | } else { 227 | schema = nil; 228 | } 229 | 230 | if (!schema) { 231 | return NO; 232 | } 233 | } 234 | BOOL result = [self _validateJSON:json withSchemaDict:schema]; 235 | if (newDocument) { 236 | [self removeResolution]; 237 | } 238 | return result; 239 | } 240 | 241 | -(BOOL)setResolutionString:(NSString *)resolution forSchema:(NSDictionary *)schema 242 | { 243 | //res and schema as Pair only add if different to previous. pop smart. pre fill. leave ability to look up res anywhere. 244 | //we should warn if the resolution contains a JSON-Pointer (these are a bad idea in an ID) 245 | NSURL *baseURL = (self.resolutionStack.lastObject) ? (self.resolutionStack.lastObject) : [NSURL URLWithString:@""]; 246 | NSURL *fullURL = [NSURL URLWithString:resolution relativeToURL:baseURL]; 247 | NSURL *idURI = [self urlWithoutFragment:fullURL]; 248 | 249 | return [self setResolutionUrl:idURI forSchema:schema]; 250 | } 251 | 252 | -(BOOL)setResolutionUrl:(NSURL *)idURI forSchema:(NSDictionary *)schema { 253 | if (!([self.resolutionStack.lastObject isEqual:idURI] && [self.schemaStack.lastObject isEqual:schema])) { 254 | [self.resolutionStack addObject:idURI]; 255 | [self.schemaStack addObject:schema]; 256 | return YES; 257 | } 258 | return NO; 259 | } 260 | 261 | -(void)removeResolution 262 | { 263 | [self.resolutionStack removeLastObject]; 264 | [self.schemaStack removeLastObject]; 265 | } 266 | 267 | -(BOOL)validateJSONInstance:(id)json withSchemaData:(NSData*)schemaData 268 | { 269 | NSError * error = nil; 270 | NSString * jsonKey = nil; 271 | if (![NSJSONSerialization isValidJSONObject:json]) { 272 | #ifdef DEBUG 273 | //in order to pass the tests 274 | jsonKey = @"debugInvalidTopTypeKey"; 275 | json = @{jsonKey : json}; 276 | // schema = @{@"properties" : @{@"debugInvalidTopTypeKey" : schema}}; 277 | #else 278 | return NO; 279 | #endif 280 | } 281 | NSData * jsonData = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; 282 | if (error != nil) { 283 | return NO; 284 | } 285 | return [self validateJSONData:jsonData withKey:jsonKey withSchemaData:schemaData]; 286 | } 287 | 288 | -(BOOL)validateJSONInstance:(id)json withSchema:(NSDictionary*)schema; 289 | { 290 | NSError * error = nil; 291 | NSString * jsonKey = nil; 292 | if (![NSJSONSerialization isValidJSONObject:json]) { 293 | #ifdef DEBUG 294 | //in order to pass the tests 295 | jsonKey = @"debugInvalidTopTypeKey"; 296 | json = @{jsonKey : json}; 297 | // schema = @{@"properties" : @{@"debugInvalidTopTypeKey" : schema}}; 298 | #else 299 | return NO; 300 | #endif 301 | } 302 | NSData * jsonData = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; 303 | if (error != nil) { 304 | return NO; 305 | } 306 | NSData * schemaData = [NSJSONSerialization dataWithJSONObject:schema options:0 error:&error]; 307 | if (error != nil) { 308 | return NO; 309 | } 310 | return [self validateJSONData:jsonData withKey:jsonKey withSchemaData:schemaData]; 311 | } 312 | 313 | -(BOOL)validateJSONData:(NSData*)jsonData withSchemaData:(NSData*)schemaData 314 | { 315 | return [self validateJSONData:jsonData withKey:nil withSchemaData:schemaData]; 316 | } 317 | 318 | -(BOOL)validateJSONData:(NSData*)jsonData withKey:(NSString*)key withSchemaData:(NSData*)schemaData 319 | { 320 | NSError * error = nil; 321 | id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; 322 | if (error != nil) { 323 | return NO; 324 | } 325 | if (key != nil) { 326 | json = json[key]; 327 | } 328 | id schema = [NSJSONSerialization JSONObjectWithData:schemaData options:0 error:&error]; 329 | if (error != nil) { 330 | return NO; 331 | } 332 | if (![schema isKindOfClass:[NSDictionary class]]) { 333 | return NO; 334 | } 335 | if (![self validateJSON:json withSchemaDict:schema]) { 336 | return NO; 337 | } 338 | return YES; 339 | } 340 | 341 | -(BOOL)validateJSON:(id)json withSchemaDict:(NSDictionary *)schema 342 | { 343 | @synchronized(self) 344 | { 345 | if (!schema || 346 | ![schema isKindOfClass:[NSDictionary class]]) { 347 | //NSLog(@"No schema specified, or incorrect data type: %@", schema); 348 | return NO; 349 | } 350 | 351 | //need to make sure the validation of schema doesn't infinitely recurse (self references) 352 | // therefore should not expand any subschemas, and ensure schema are only checked on a 'top' level. 353 | //first validate the schema against the root schema then validate against the original 354 | //first check valid json (use NSJSONSerialization) 355 | 356 | self.validationStack = [NSMutableArray new]; 357 | self.resolutionStack = [NSMutableArray new]; 358 | self.schemaStack = [NSMutableArray new]; 359 | 360 | [self setResolutionString:@"#" forSchema:schema]; 361 | 362 | if (![self _validateJSON:schema withSchemaDict:self.rootSchema]) { 363 | return NO; //error: invalid schema 364 | } 365 | if (![self _validateJSON:json withSchemaDict:schema]) { 366 | return NO; 367 | } 368 | 369 | [self removeResolution]; 370 | return YES; 371 | } 372 | } 373 | 374 | -(BOOL)_validateJSON:(id)json withSchemaDict:(NSDictionary *)schema 375 | { 376 | NSParameterAssert(schema != nil); 377 | //check stack for JSON and schema 378 | //push to stack the json and the schema. 379 | if (![self pushToStackJSON:json forSchema:schema]) { 380 | return NO; 381 | } 382 | BOOL newResolution = NO; 383 | NSString *resolutionValue = schema[@"id"]; 384 | if (resolutionValue) { 385 | newResolution = [self setResolutionString:resolutionValue forSchema:schema]; 386 | } 387 | BOOL result = [self __validateJSON:json withSchemaDict:schema]; 388 | //pop from the stacks 389 | if (newResolution) { 390 | [self removeResolution]; 391 | } 392 | [self popStack]; 393 | return result; 394 | } 395 | 396 | -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema 397 | { 398 | //TODO: synonyms (potentially in higher level too) 399 | 400 | static NSArray * anyInstanceKeywords; 401 | static NSArray * allKeywords; 402 | static dispatch_once_t onceToken; 403 | dispatch_once(&onceToken, ^{ 404 | anyInstanceKeywords = @[@"enum", @"type", @"allOf", @"anyOf", @"oneOf", @"not", @"definitions"]; 405 | allKeywords = @[@"multipleOf", @"maximum", @"exclusiveMaximum", @"minimum", @"exclusiveMinimum", 406 | @"maxLength", @"minLength", @"pattern", 407 | @"maxProperties", @"minProperties", @"required", @"properties", @"patternProperties", @"additionalProperties", @"dependencies", 408 | @"additionalItems", @"items", @"maxItems", @"minItems", @"uniqueItems", 409 | @"enum", @"type", @"allOf", @"anyOf", @"oneOf", @"not", @"definitions"]; 410 | }); 411 | //The "id" keyword (or "id", for short) is used to alter the resolution scope. When an id is encountered, an implementation MUST resolve this id against the most immediate parent scope. The resolved URI will be the new resolution scope for this subschema and all its children, until another id is encountered. 412 | 413 | /*"title" and "description" 414 | 6.1.1. Valid values 415 | 6.1.2. Purpose 416 | 6.2. "default" 417 | format <- optional*/ 418 | 419 | /* Defaults */ 420 | //the strategy for defaults is to dive one deeper and replace *just* ahead of where we are 421 | // for (NSString * keyword in allKeywords) { 422 | // if ([schema[keyword] isKindOfClass:[NSDictionary class]] && schema[keyword][@"default"] != nil && [json isKindOfClass:[NSDictionary class]] && [json objectForKey:keyword] == nil) {//this only does shallow defaults replacement 423 | // [json setObject:[schema[keyword][@"default"] mutableCopy] forKey:keyword]; 424 | // } 425 | // } 426 | 427 | if (schema[@"$ref"]) { 428 | if (![schema[@"$ref"] isKindOfClass:[NSString class]]) { 429 | return NO; 430 | } 431 | return [self validateJSON:json withSchemaAtReference:schema[@"$ref"]]; 432 | } 433 | 434 | NSString *type = nil; 435 | SEL typeValidator = nil; 436 | if ([json isKindOfClass:[NSArray class]]) { 437 | type = @"array"; 438 | typeValidator = @selector(_validateJSONArray:withSchemaDict:); 439 | } else if ([json isKindOfClass:[NSNumber class]]) { 440 | NSParameterAssert(strcmp( [@YES objCType], @encode(char) ) == 0); 441 | if (strcmp( [json objCType], @encode(char) ) == 0) { 442 | type = @"boolean"; 443 | } else { 444 | typeValidator = @selector(_validateJSONNumeric:withSchemaDict:); 445 | double num = [json doubleValue]; 446 | if ((num - floor(num)) == 0.0) { 447 | type = @"integer"; 448 | } else { 449 | type = @"number"; 450 | } 451 | } 452 | } else if ([json isKindOfClass:[NSNull class]]) { 453 | type = @"null"; 454 | } else if ([json isKindOfClass:[NSDictionary class]]) { 455 | type = @"object"; 456 | typeValidator = @selector(_validateJSONObject:withSchemaDict:); 457 | 458 | } else if ([json isKindOfClass:[NSString class]]) { 459 | type = @"string"; 460 | typeValidator = @selector(_validateJSONString:withSchemaDict:); 461 | } else { 462 | return NO; // the schema is not one of the valid types. 463 | } 464 | 465 | //TODO: extract the types first before the check (if there is no type specified, we'll never hit the checking code 466 | for (NSString * keyword in anyInstanceKeywords) { 467 | id schemaItem = schema[keyword]; 468 | if (schemaItem != nil) { 469 | 470 | if ([keyword isEqualToString:@"enum"]) { 471 | //An instance validates successfully against this keyword if its value is equal to one of the elements in this keyword's array value. 472 | if (![schemaItem containsObject:json]) { 473 | return NO; 474 | } 475 | } else if ([keyword isEqualToString:@"type"]) { 476 | if ([schemaItem isKindOfClass:[NSString class]]) { 477 | if ([type isEqualToString:@"integer"] && [schemaItem isEqualToString:@"number"]) { 478 | continue; 479 | } 480 | if (![schemaItem isEqualToString:type]) { 481 | return NO; 482 | } 483 | } else { //array 484 | if (![schemaItem containsObject:type]) { 485 | return NO; 486 | } 487 | } 488 | } else if ([keyword isEqualToString:@"allOf"]) { 489 | for (NSDictionary * subSchema in schemaItem) { 490 | if (![self _validateJSON:json withSchemaDict:subSchema]) { return NO; } 491 | } 492 | } else if ([keyword isEqualToString:@"anyOf"]) { 493 | BOOL anySuccess = NO; 494 | for (NSDictionary * subSchema in schemaItem) { 495 | if ([self _validateJSON:json withSchemaDict:subSchema]) { 496 | anySuccess = YES; 497 | break; 498 | } 499 | } 500 | if (!anySuccess) { 501 | return NO; 502 | } 503 | } else if ([keyword isEqualToString:@"oneOf"]) { 504 | int passes = 0; 505 | for (NSDictionary * subSchema in schemaItem) { 506 | if ([self _validateJSON:json withSchemaDict:subSchema]) { passes++; } 507 | if (passes > 1) { return NO; } 508 | } 509 | if (passes != 1) { return NO; } 510 | } else if ([keyword isEqualToString:@"not"]) { 511 | if ([self _validateJSON:json withSchemaDict:schemaItem]) { return NO; } 512 | } else if ([keyword isEqualToString:@"definitions"]) { 513 | 514 | } 515 | } 516 | } 517 | 518 | if (typeValidator != nil) { 519 | IMP imp = [self methodForSelector:typeValidator]; 520 | BOOL (*func)(id, SEL, id, id) = (BOOL(*)(id, SEL, id, id))imp; 521 | if (!func(self, typeValidator, json, schema)) { 522 | return NO; 523 | } 524 | } 525 | 526 | return YES; 527 | } 528 | 529 | //for number and integer 530 | -(BOOL)_validateJSONNumeric:(NSNumber*)jsonNumber withSchemaDict:(NSDictionary*)schema 531 | { 532 | static NSArray * numericKeywords; 533 | static dispatch_once_t onceToken; 534 | dispatch_once(&onceToken, ^{ 535 | numericKeywords = @[@"multipleOf", @"maximum",/* @"exclusiveMaximum",*/ @"minimum",/* @"exclusiveMinimum"*/]; 536 | }); 537 | 538 | if (!schema || ![schema isKindOfClass:[NSDictionary class]]) { 539 | return NO; 540 | } 541 | 542 | for (NSString * keyword in numericKeywords) { 543 | id schemaItem = schema[keyword]; 544 | if (schemaItem != nil) { 545 | 546 | if ([keyword isEqualToString:@"multipleOf"]) { 547 | //A numeric instance is valid against "multipleOf" if the result of the division of the instance by this keyword's value is an integer. 548 | double divResult = [jsonNumber doubleValue] / [schemaItem doubleValue]; 549 | if ((divResult - floor(divResult)) != 0.0) { 550 | return NO; 551 | } 552 | } else if ([keyword isEqualToString:@"maximum"]) { 553 | if ([schema[@"exclusiveMaximum"] isKindOfClass:[NSNumber class]] && [schema[@"exclusiveMaximum"] boolValue] == YES) { 554 | if (!([jsonNumber doubleValue] < [schemaItem doubleValue])) { 555 | //if "exclusiveMaximum" has boolean value true, the instance is valid if it is strictly lower than the value of "maximum". 556 | return NO; 557 | } 558 | } else { 559 | if (!([jsonNumber doubleValue] <= [schemaItem doubleValue])) { 560 | //if "exclusiveMaximum" is not present, or has boolean value false, then the instance is valid if it is lower than, or equal to, the value of "maximum" 561 | return NO; 562 | } 563 | } 564 | } else if ([keyword isEqualToString:@"minimum"]) { 565 | if ([schema[@"exclusiveMinimum"] isKindOfClass:[NSNumber class]] && [schema[@"exclusiveMinimum"] boolValue] == YES) { 566 | if (!([jsonNumber doubleValue] > [schemaItem doubleValue])) { 567 | //if "exclusiveMinimum" is present and has boolean value true, the instance is valid if it is strictly greater than the value of "minimum". 568 | return NO; 569 | } 570 | } else { 571 | if (!([jsonNumber doubleValue] >= [schemaItem doubleValue])) { 572 | //if "exclusiveMinimum" is not present, or has boolean value false, then the instance is valid if it is greater than, or equal to, the value of "minimum" 573 | return NO; 574 | } 575 | } 576 | } 577 | } 578 | } 579 | return YES; 580 | } 581 | 582 | -(BOOL)_validateJSONString:(NSString*)jsonString withSchemaDict:(NSDictionary*)schema 583 | { 584 | static NSArray * stringKeywords; 585 | static dispatch_once_t onceToken; 586 | dispatch_once(&onceToken, ^{ 587 | stringKeywords = @[@"maxLength", @"minLength", @"pattern"]; 588 | }); 589 | 590 | for (NSString * keyword in stringKeywords) { 591 | id schemaItem = schema[keyword]; 592 | if (schemaItem != nil) { 593 | 594 | if ([keyword isEqualToString:@"maxLength"]) { 595 | //A string instance is valid against this keyword if its length is less than, or equal to, the value of this keyword. 596 | 597 | //What's going on here - [NSString length] returns the number of unichars in a string. Unichars are 16bit but 598 | // surrogate pairs in unicode require to Unichars. This is more common as this is how emoji are encoded. 599 | // Go read this if you care: http://www.objc.io/issue-9/unicode.html (See Common Pitfalls - Length) 600 | NSInteger realLength = [jsonString lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4; 601 | 602 | if (!(realLength <= [schemaItem integerValue])) { return NO; } 603 | } else if ([keyword isEqualToString:@"minLength"]) { 604 | //A string instance is valid against this keyword if its length is greater than, or equal to, the value of this keyword. 605 | 606 | NSInteger realLength = [jsonString lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4; 607 | if (!(realLength >= [schemaItem intValue])) { return NO; } 608 | } else if ([keyword isEqualToString:@"pattern"]) { 609 | //A string instance is considered valid if the regular expression matches the instance successfully. Recall: regular expressions are not implicitly anchored. 610 | //This string SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect. 611 | //NOTE: this regex uses ICU which has some differences to ECMA-262 (such as look-behind) 612 | NSError * error; 613 | NSRegularExpression * regex = [NSRegularExpression regularExpressionWithPattern:schemaItem options:0 error:&error]; 614 | if (error) { 615 | continue; 616 | } 617 | if (NSEqualRanges([regex rangeOfFirstMatchInString:jsonString options:0 range:NSMakeRange(0, jsonString.length)], NSMakeRange(NSNotFound, 0))) { 618 | //A string instance is considered valid if the regular expression matches the instance successfully. Recall: regular expressions are not implicitly anchored. 619 | return NO; 620 | } 621 | } 622 | } 623 | } 624 | return YES; 625 | } 626 | 627 | -(BOOL)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDictionary*)schema 628 | { 629 | static NSArray * objectKeywords; 630 | static dispatch_once_t onceToken; 631 | dispatch_once(&onceToken, ^{ 632 | objectKeywords = @[@"maxProperties", @"minProperties", @"required", @"properties", @"patternProperties", @"additionalProperties", @"dependencies"]; 633 | }); 634 | BOOL doneProperties = NO; 635 | for (NSString * keyword in objectKeywords) { 636 | id schemaItem = schema[keyword]; 637 | if (schemaItem != nil) { 638 | 639 | if ([keyword isEqualToString:@"maxProperties"]) { 640 | //An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword. 641 | if ((NSInteger)[jsonDict count] > [schemaItem integerValue]) { return NO; /*invalid JSON dict*/ } 642 | } else if ([keyword isEqualToString:@"minProperties"]) { 643 | //An object instance is valid against "minProperties" if its number of properties is greater than, or equal to, the value of this keyword. 644 | if ((NSInteger)[jsonDict count] < [schemaItem integerValue]) { return NO; /*invalid JSON dict*/ } 645 | } else if ([keyword isEqualToString:@"required"]) { 646 | NSArray * requiredArray = schemaItem; 647 | for (NSObject * requiredProp in requiredArray) { 648 | NSString * requiredPropStr = (NSString*)requiredProp; 649 | if (![jsonDict valueForKey:requiredPropStr]) { 650 | return NO; //required not present. invalid JSON dict. 651 | } 652 | } 653 | } else if (!doneProperties && ([keyword isEqualToString:@"properties"] || [keyword isEqualToString:@"patternProperties"] || [keyword isEqualToString:@"additionalProperties"])) { 654 | doneProperties = YES; 655 | NSDictionary * properties = schema[@"properties"]; 656 | NSDictionary * patternProperties = schema[@"patternProperties"]; 657 | id additionalProperties = schema[@"additionalProperties"]; 658 | if (properties == nil) { properties = [NSDictionary new]; } 659 | if (patternProperties == nil) { patternProperties = [NSDictionary new]; } 660 | if (additionalProperties == nil || ([additionalProperties isKindOfClass:[NSNumber class]] && strcmp([additionalProperties objCType], @encode(char)) == 0 && [additionalProperties boolValue] == YES)) { 661 | additionalProperties = [NSDictionary new]; 662 | } 663 | 664 | /** calculating children schemas **/ 665 | //The calculation of the children schemas is combined with the checking of present keys 666 | NSSet * p = [NSSet setWithArray:[properties allKeys]]; 667 | NSArray * pp = [patternProperties allKeys]; 668 | NSSet * allKeys = [NSSet setWithArray:[jsonDict allKeys]]; 669 | NSMutableDictionary * testSchemas = [NSMutableDictionary dictionaryWithCapacity:allKeys.count]; 670 | 671 | NSMutableSet * ps = [NSMutableSet setWithSet:allKeys]; 672 | //If set "p" contains value "m", then the corresponding schema in "properties" is added to "s". 673 | [ps intersectSet:p]; 674 | for (id m in ps) { 675 | testSchemas[m] = [NSMutableArray arrayWithObject:[properties objectForKey:m]]; 676 | } 677 | 678 | //we loop the regexes so each is only created once 679 | //For each regex in "pp", if it matches "m" successfully, the corresponding schema in "patternProperties" is added to "s". 680 | for (NSString * regexString in pp) { 681 | //Each property name of this object SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect. 682 | //NOTE: this regex uses ICU which has some differences to ECMA-262 (such as look-behind) 683 | NSError * error; 684 | NSRegularExpression * regex = [NSRegularExpression regularExpressionWithPattern:regexString options:0 error:&error]; 685 | if (error) { 686 | continue; 687 | } 688 | for (NSString * m in allKeys) { 689 | if (!NSEqualRanges([regex rangeOfFirstMatchInString:m options:0 range:NSMakeRange(0, m.length)], NSMakeRange(NSNotFound, 0))) { 690 | if (testSchemas[m] == NULL) { 691 | testSchemas[m] = [NSMutableArray arrayWithObject:[patternProperties objectForKey:regexString]]; 692 | } else { 693 | [testSchemas[m] addObject:[patternProperties objectForKey:regexString]]; 694 | } 695 | } 696 | } 697 | } 698 | NSParameterAssert(testSchemas.count <= allKeys.count); 699 | 700 | //Successful validation of an object instance against these three keywords depends on the value of "additionalProperties": 701 | // if its value is boolean true or a schema, validation succeeds; 702 | // if its value is boolean false, the algorithm to determine validation success is described below. 703 | if ([additionalProperties isKindOfClass:[NSNumber class]] && [additionalProperties boolValue] == NO) { //value must therefore be boolean false 704 | //Because we have built a set of schemas/keys up (rather than down), the following test is equivalent to the requirement: 705 | //Validation of the instance succeeds if, after these two steps, set "s" is empty. 706 | if (testSchemas.count < allKeys.count) { 707 | return NO; 708 | } 709 | } else { 710 | //find keys from allkeys that are not in testSchemas and add additionalProperties 711 | NSDictionary * additionalPropsSchema = nil; 712 | //In addition, boolean value true for "additionalItems" is considered equivalent to an empty schema. 713 | 714 | if ([additionalProperties isKindOfClass:[NSNumber class]] && strcmp([additionalProperties objCType], @encode(char)) == 0) { 715 | additionalPropsSchema = [NSDictionary new]; 716 | } else { 717 | additionalPropsSchema = additionalProperties; 718 | } 719 | NSMutableSet * additionalKeys = [allKeys mutableCopy]; 720 | [additionalKeys minusSet:[testSchemas keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) { return YES; }]]; 721 | for (NSString * key in additionalKeys) { 722 | testSchemas[key] = [NSMutableArray arrayWithObject:additionalPropsSchema]; 723 | } 724 | } 725 | 726 | //TODO: run the tests on the testSchemas 727 | for (NSString * property in [testSchemas keyEnumerator]) { 728 | NSArray * subschemas = testSchemas[property]; 729 | for (NSDictionary * subschema in subschemas) { 730 | if (![self _validateJSON:jsonDict[property] withSchemaDict:subschema]) { 731 | return NO; 732 | } 733 | } 734 | } 735 | } else if ([keyword isEqualToString:@"dependencies"]) { 736 | NSSet * properties = [jsonDict keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) { return YES; }]; 737 | NSDictionary * dependencies = schemaItem; 738 | for (NSString * name in [dependencies allKeys]) { 739 | if (![properties containsObject:name]) { 740 | continue; 741 | } 742 | 743 | id dependency = dependencies[name]; 744 | if ([dependency isKindOfClass:[NSDictionary class]]) { 745 | NSDictionary * schemaDependency = dependency; 746 | //For all (name, schema) pair of schema dependencies, if the instance has a property by this name, then it must also validate successfully against the schema. 747 | //Note that this is the instance itself which must validate successfully, not the value associated with the property name. 748 | if (![self _validateJSON:jsonDict withSchemaDict:schemaDependency]) { 749 | return NO; 750 | } 751 | } else if ([dependency isKindOfClass:[NSArray class]]) { 752 | NSArray * propertyDependency = dependency; 753 | //For each (name, propertyset) pair of property dependencies, if the instance has a property by this name, then it must also have properties with the same names as propertyset. 754 | NSSet * propertySet = [NSSet setWithArray:propertyDependency]; 755 | if (![propertySet isSubsetOfSet:properties]) { 756 | return NO; 757 | } 758 | } 759 | } 760 | } 761 | } 762 | } 763 | return YES; 764 | } 765 | 766 | -(BOOL)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*)schema 767 | { 768 | static NSArray * arrayKeywords; 769 | static dispatch_once_t onceToken; 770 | dispatch_once(&onceToken, ^{ 771 | arrayKeywords = @[@"additionalItems", @"items", @"maxItems", @"minItems", @"uniqueItems"]; 772 | }); 773 | 774 | BOOL doneItems = NO; 775 | for (NSString * keyword in arrayKeywords) { 776 | id schemaItem = schema[keyword]; 777 | if (schemaItem != nil) { 778 | 779 | if (!doneItems && ([keyword isEqualToString:@"additionalItems"] || [keyword isEqualToString:@"items"])) { 780 | doneItems = YES; 781 | id additionalItems = schema[@"additionalItems"]; 782 | id items = schema[@"items"]; 783 | if (additionalItems == nil) { additionalItems = [NSDictionary new];} 784 | if (items == nil) { items = [NSDictionary new];} 785 | if ([additionalItems isKindOfClass:[NSNumber class]] && strcmp([additionalItems objCType], @encode(char)) == 0 && [additionalItems boolValue] == YES) { 786 | additionalItems = [NSDictionary new]; 787 | } 788 | 789 | for (NSUInteger index = 0; index < [jsonArray count]; index++) { 790 | id child = jsonArray[index]; 791 | if ([items isKindOfClass:[NSDictionary class]]) { 792 | //If items is a schema, then the child instance must be valid against this schema, regardless of its index, and regardless of the value of "additionalItems". 793 | if (![self _validateJSON:jsonArray[index] withSchemaDict:items]) { 794 | return NO; 795 | } 796 | } else if ([items isKindOfClass:[NSArray class]]) { 797 | if (index < [(NSArray *)items count]) { 798 | if (![self _validateJSON:child withSchemaDict:items[index]]) { 799 | return NO; 800 | } 801 | } else { 802 | if ([additionalItems isKindOfClass:[NSNumber class]] && [additionalItems boolValue] == NO) { 803 | //if the value of "additionalItems" is boolean value false and the value of "items" is an array, the instance is valid if its size is less than, or equal to, the size of "items". 804 | return NO; 805 | } else { 806 | if (![self _validateJSON:child withSchemaDict:additionalItems]) { 807 | return NO; 808 | } 809 | } 810 | } 811 | } 812 | } 813 | } else if ([keyword isEqualToString:@"maxItems"]) { 814 | //An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword. 815 | if ((NSInteger)[jsonArray count] > [schemaItem integerValue]) { return NO; } 816 | //An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. 817 | } else if ([keyword isEqualToString:@"minItems"]) { 818 | if ((NSInteger)[jsonArray count] < [schemaItem integerValue]) { return NO; } 819 | } else if ([keyword isEqualToString:@"uniqueItems"]) { 820 | if ([schemaItem isKindOfClass:[NSNumber class]] && [schemaItem boolValue] == YES) { 821 | //If it has boolean value true, the instance validates successfully if all of its elements are unique. 822 | NSSet * uniqueItems = [NSSet setWithArray:jsonArray]; 823 | 824 | NSUInteger fudgeFactor = 0; 825 | if ([self valuesHaveOneAndTrue:jsonArray]) 826 | { 827 | fudgeFactor++; 828 | } 829 | 830 | // false and zero are treated as unique 831 | if ([self valuesHaveZeroAndFalse:jsonArray]) 832 | { 833 | fudgeFactor++; 834 | } 835 | 836 | if (([uniqueItems count] + fudgeFactor) < [jsonArray count]) 837 | { 838 | return NO; 839 | } 840 | } 841 | } 842 | } 843 | } 844 | return YES; 845 | } 846 | 847 | - (BOOL)valuesHaveOneAndTrue:(NSArray *)values 848 | { 849 | BOOL trueFound = NO; 850 | BOOL oneFound = NO; 851 | 852 | for (NSNumber *number in values) { 853 | if (![number isKindOfClass:[NSNumber class]]) { 854 | continue; 855 | } 856 | 857 | if (strcmp([number objCType], @encode(char)) == 0) { 858 | if ([number boolValue] == YES) { 859 | trueFound = YES; 860 | } 861 | } else if ([number doubleValue] == 1.0) { 862 | oneFound = YES; 863 | } 864 | } 865 | return (trueFound && oneFound); 866 | } 867 | 868 | - (BOOL)valuesHaveZeroAndFalse:(NSArray *)values 869 | { 870 | BOOL falseFound = NO; 871 | BOOL zeroFound = NO; 872 | 873 | for (NSNumber *number in values) 874 | { 875 | if (![number isKindOfClass:[NSNumber class]]) { 876 | continue; 877 | } 878 | if (strcmp([number objCType], @encode(char)) == 0) { 879 | if ([number boolValue] == NO) { 880 | falseFound = YES; 881 | } 882 | } else if ([number doubleValue] == 0.0) { 883 | zeroFound = YES; 884 | } 885 | } 886 | return (falseFound && zeroFound); 887 | } 888 | 889 | -(BOOL)checkSchemaRef:(NSDictionary*)schema 890 | { 891 | NSArray * validSchemaArray = @[ 892 | @"http://json-schema.org/schema#", 893 | //JSON Schema written against the current version of the specification. 894 | //@"http://json-schema.org/hyper-schema#", 895 | //JSON Schema written against the current version of the specification. 896 | @"http://json-schema.org/draft-04/schema#", 897 | //JSON Schema written against this version. 898 | @"http://json-schema.org/draft-04/hyper-schema#", 899 | //JSON Schema hyperschema written against this version. 900 | //@"http://json-schema.org/draft-03/schema#", 901 | //JSON Schema written against JSON Schema, draft v3 [json‑schema‑03]. 902 | //@"http://json-schema.org/draft-03/hyper-schema#" 903 | //JSON Schema hyperschema written against JSON Schema, draft v3 [json‑schema‑03]. 904 | ]; 905 | 906 | if ([validSchemaArray containsObject:schema[@"$schema"]]) { 907 | return YES; 908 | } else { 909 | return NO; //invalid schema - although technically including $schema is only RECOMMENDED 910 | } 911 | } 912 | 913 | @end 914 | -------------------------------------------------------------------------------- /Sources/KiteValidationPair.h: -------------------------------------------------------------------------------- 1 | // 2 | // KiteValidationPair.h 3 | // Tests 4 | // 5 | // Created by Sam Duke on 24/01/2014. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface KiteValidationPair : NSObject 12 | @property (nonatomic, readonly) NSObject* left; 13 | @property (nonatomic, readonly) NSObject* right; 14 | + (instancetype) pairWithLeft:(NSObject *)l right:(NSObject *)r; 15 | - (instancetype) initWithLeft:(NSObject *)l right:(NSObject *)r; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Sources/KiteValidationPair.m: -------------------------------------------------------------------------------- 1 | // 2 | // KiteValidationPair.m 3 | // Tests 4 | // 5 | // Created by Sam Duke on 24/01/2014. 6 | // 7 | // 8 | 9 | #import "KiteValidationPair.h" 10 | 11 | @implementation KiteValidationPair 12 | 13 | @synthesize left=_left; 14 | @synthesize right=_right; 15 | 16 | + (instancetype)pairWithLeft:(NSObject*)l right:(NSObject*)r { 17 | return [[[self class] alloc] initWithLeft:l right:r]; 18 | } 19 | 20 | - (instancetype)initWithLeft:(NSObject*)l right:(NSObject*)r { 21 | if (self = [super init]) { 22 | _left = [l copy]; 23 | _right = [r copy]; 24 | } 25 | return self; 26 | } 27 | 28 | #pragma clang diagnostic push 29 | #pragma clang diagnostic ignored "-Wunused-parameter" 30 | - (id)copyWithZone:(NSZone *)zone { 31 | return [[[self class] alloc] initWithLeft:[self left] right:[self right]]; 32 | } 33 | #pragma clang diagnostic pop 34 | 35 | - (BOOL)isEqual:(id)obj 36 | { 37 | if (![obj isKindOfClass:[KiteValidationPair class]]) 38 | return NO; 39 | 40 | KiteValidationPair *other = (KiteValidationPair *)obj; 41 | BOOL isLeftEqual = (_left == other->_left || 42 | [_left isEqual:other->_left]); 43 | BOOL isRightEqual = (_right == other->_right || 44 | [_right isEqual:other->_right]); 45 | 46 | return (isLeftEqual && isRightEqual); 47 | } 48 | 49 | #define NSUINT_BIT (CHAR_BIT * sizeof(NSUInteger)) 50 | #define NSUINTROTATE(val, howmuch) ((((NSUInteger)val) << howmuch) | (((NSUInteger)val) >> (NSUINT_BIT - howmuch))) 51 | 52 | - (NSUInteger)hash 53 | { 54 | return NSUINTROTATE([_left hash], NSUINT_BIT / 2) ^ [_right hash]; 55 | } 56 | 57 | - (NSString *)description 58 | { 59 | return [NSString stringWithFormat:@"Left -> %@\nRight -> %@", _left, _right]; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /Tests/KiteJSONValidatorTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // Tests.m 3 | // Tests 4 | // 5 | // Created by Sam Duke on 19/01/2014. 6 | // 7 | // 8 | 9 | #import 10 | #import "KiteJSONValidator.h" 11 | 12 | @interface Tests : XCTestCase 13 | 14 | @end 15 | 16 | @implementation Tests 17 | 18 | - (void)setUp 19 | { 20 | [super setUp]; 21 | // Put setup code here. This method is called before the invocation of each test method in the class. 22 | } 23 | 24 | - (void)tearDown 25 | { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | [super tearDown]; 28 | } 29 | 30 | - (void)testDraft4Suite 31 | { 32 | NSBundle * mainBundle = [NSBundle bundleForClass:[self class]]; 33 | NSArray * paths = [mainBundle pathsForResourcesOfType:@"json" inDirectory:@"JSON-Schema-Test-Suite/tests/draft4"]; 34 | NSString * directory = [[mainBundle resourcePath] stringByAppendingPathComponent:@"JSON-Schema-Test-Suite/remotes"]; 35 | NSArray * refPaths = [self recursivePathsForResourcesOfType:@"json" inDirectory:directory]; 36 | 37 | unsigned int successes = 0; 38 | 39 | for (NSString * path in paths) { 40 | NSData *testData = [NSData dataWithContentsOfFile:path]; 41 | NSError *error = nil; 42 | NSDictionary * tests = [NSJSONSerialization JSONObjectWithData:testData 43 | options:kNilOptions 44 | error:&error]; 45 | if (error != nil) { 46 | XCTFail(@"Failed to load test file: %@", path); 47 | continue; 48 | } 49 | 50 | for (NSDictionary * test in tests) { 51 | for (NSDictionary * json in test[@"tests"]) { 52 | KiteJSONValidator * validator = [KiteJSONValidator new]; 53 | for (NSString * refPath in refPaths) 54 | { 55 | NSString * fullpath = [directory stringByAppendingPathComponent:refPath]; 56 | NSData * data = [NSData dataWithContentsOfFile:fullpath]; 57 | NSURL * url = [NSURL URLWithString:@"http://localhost:1234/"]; 58 | url = [NSURL URLWithString:refPath relativeToURL:url]; 59 | BOOL success = [validator addRefSchemaData:data atURL:url]; 60 | XCTAssertTrue(success == YES, @"Unable to add the reference schema at '%@'", url); 61 | } 62 | 63 | BOOL result = [validator validateJSONInstance:json[@"data"] withSchema:test[@"schema"]]; 64 | BOOL desired = [json[@"valid"] boolValue]; 65 | if (result != desired) { 66 | XCTFail(@"Category: %@ Test: %@ Expected result: %i", test[@"description"], json[@"description"], desired); 67 | } 68 | else 69 | { 70 | successes++; 71 | } 72 | } 73 | } 74 | } 75 | 76 | XCTAssertTrue(successes >= 251, @"Expected at least 251 test successes (as of draft v4), but found %ud", successes); 77 | } 78 | 79 | - (NSArray *)recursivePathsForResourcesOfType:(NSString *)type inDirectory:(NSString *)directoryPath { 80 | NSMutableArray *filePaths = [[NSMutableArray alloc] init]; 81 | NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:directoryPath]; 82 | NSString *filePath = nil; 83 | 84 | while ((filePath = [enumerator nextObject]) != nil) { 85 | if (!type || [[filePath pathExtension] isEqualToString:type]){ 86 | [filePaths addObject:filePath]; 87 | } 88 | } 89 | 90 | return filePaths; 91 | } 92 | 93 | @end 94 | -------------------------------------------------------------------------------- /Tests/Tests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | samskiter.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/Tests-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #ifdef __OBJC__ 8 | #import 9 | #import 10 | #endif 11 | -------------------------------------------------------------------------------- /Tests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | --------------------------------------------------------------------------------