XSD 검사기
XSD로 검사합니다. 두 파일 모두 브라우저를 벗어나지 않습니다.
모든 처리는 이 탭 안에서 이루어집니다. 붙여넣은 내용은 업로드되거나 기록되거나 전송되지 않습니다. 네트워크 패널을 열어 확인하세요.
왼쪽에 문서를, 오른쪽에 해당 XSD를 붙여넣고 "스키마로 검증"을 누르세요. WebAssembly로 컴파일한 libxml2가 이 탭의 Web Worker에서 검사를 실행하고, 모든 위반 사항이 줄과 열, 그리고 누르면 그 자리로 이동하는 표시와 함께 나열됩니다.
보통은 다른 무언가가 문서를 거부한 뒤에 여기까지 옵니다. 거래처가 cvc-complex-type.2.4.a를 돌려줬거나, 설정 파일에서 빌드가 실패했거나, 게이트웨이가 페이로드가 스키마와 맞지 않는다며 거기서 멈췄거나. 여러분에게는 XSD가 있고, 필요한 것은 실패한 요소이지 Java 툴체인이 아닙니다.
차이는 어디서 실행되느냐입니다. 브라우저에는 스키마 검사기가 내장되어 있지 않아서, XSD 검증을 제공하는 무료 도구들은 두 파일을 모두 서버로 보냅니다. 가장 잘 알려진 곳은 XML을 먼저 제출하게 하고 스키마는 두 번째 양식에서 받습니다. 여기서는 어느 파일도 이 탭을 벗어나지 않으며, 엔진(gzip 기준 약 480 KB)은 페이지를 열 때가 아니라 버튼을 누를 때 내려받습니다.
먼저 적격 형식, 그다음 유효성
이것은 하나가 아니라 두 개의 검사입니다. 적격 형식은 구문의 문제입니다. 태그가 닫혀 있고, 중첩이 올바르고, 루트가 하나이고, 앰퍼샌드가 이스케이프되어 있는지. 문서만 있으면 되므로 입력하는 동안 실행됩니다. 유효성은 적격 형식에 더해 스키마에 부합하는지이며, 두 번째 파일이 필요하므로 의도적으로 실행하는 동작으로 두었습니다.
엔진은 XSD를 파싱하고, 컴파일하고, 문서를 파싱한 뒤 검증합니다. 각 단계는 서로 다르게 실패합니다. 적격 형식이 아닌 문서는 결코 스키마에 걸리지 않습니다. 검사할 일관된 대상이 없기 때문입니다. 모든 진단에는 "스키마 문제" 또는 "유효성 오류" 표시가 붙고, 스키마 안의 위치는 일부러 누를 수 없게 했습니다. 그 줄 번호는 반대편 창의 것이기 때문입니다.
XSD 1.0만 지원하며, 그래서 빠지는 것들
libxml2가 구현한 것은 XSD 1.0입니다. XSD 1.1은 2012년 4월 5일 W3C 권고안이 되었고 Apache Xerces-J, Saxon-EE, 파이썬 xmlschema 패키지가 구현하지만, libxml2도, .NET의 XmlSchemaSet도, libxml2를 감싼 lxml도 구현하지 않습니다. 1.1 기능을 쓰는 스키마는 여기서 컴파일되지 않으며, 패널은 알 수 없는 구조 오류 대신 1.1을 유력한 원인으로 지목합니다.
- xs:assert와 xs:assertion 패싯: 한 필드의 유효성이 다른 필드에 달린 동시 발생 제약. XSD 1.0으로는 아예 표현할 수 없습니다.
- xs:alternative, 즉 조건부 타입 지정. 요소의 타입을 그 요소 자신의 속성에 대한 XPath 검사로 고르는 기능입니다.
- xs:openContent, xs:defaultOpenContent, xs:override, 그리고 1.1 데이터 타입인 xs:dateTimeStamp, xs:dayTimeDuration, xs:yearMonthDuration. 많은 글에 등장하는 xs:precisionDecimal은 최종 권고안 이전에 빠졌습니다.
- 여기에 libxml2 자체의 한계가 둘 더 있습니다. xs:redefine은 오래전부터 미구현 경로로 빠지고, xs:import는 여기서 schemaLocation을 해석할 수 없습니다. 이 빌드에서는 어떤 것도 파일을 읽을 수 없기 때문입니다. 여러 문서로 나뉜 스키마 묶음은 먼저 하나로 합치세요.
실제로 보게 될 실패들
대부분은 순서 때문입니다. xs:sequence는 자식들이 선언된 순서대로 나타나야 한다는 뜻이고, 그 순서는 서식 취향이 아니라 계약의 일부이므로 보통은 요소를 옮기는 것이 해결책입니다. XSD 1.0에서 순서를 따지지 않는 xs:all은 콘텐츠 모델의 최상위에서만, 그리고 각 요소가 최대 한 번만 나올 때에만 쓸 수 있습니다. 그래서 실제 스키마 대부분이 시퀀스를 씁니다.
나머지는 네임스페이스 때문입니다. 스키마가 targetNamespace="urn:example:orders"를 선언했다면 그것이 다스리는 모든 요소는 그 네임스페이스에 있어야 합니다. 아무것도 선언하지 않았다면 요소들은 네임스페이스가 없어야 하고, 기본 xmlns를 붙이면 깨집니다. 이웃한 함정이 elementFormDefault입니다. 없으면 unqualified를 뜻하므로 루트는 한정되고 자식들은 한정되지 않아야 합니다.
- cvc-complex-type.2.4.a: 스키마가 허용하지 않은 자리에 요소가 나타났습니다. 중괄호 안의 이름들이 그 자리에서 허용되던 것이므로, "One of {qty} is expected"는 다음 차례가 qty였다는 뜻입니다.
- cvc-complex-type.2.4.b: 콘텐츠 모델이 충족되지 않았습니다. 보통 끝부분에서 필수 자식이 빠진 경우입니다.
- cvc-datatype-valid.1.2.1: 텍스트가 해당 타입으로 어휘상 유효하지 않습니다. xs:int로 선언된 빈 요소, 초가 없는 dateTime, 천 단위 구분자가 들어간 decimal 같은 것들입니다.
- cvc-elt.1: 루트 요소의 선언을 찾지 못했습니다. 거의 언제나 선언이 없는 것이 아니라 targetNamespace가 어긋난 것입니다.
- cvc-complex-type.3.2.2: 허용되지 않는 속성입니다. XMLSchema-instance 네임스페이스를 선언하지 않고 xsi:type이나 xsi:nil을 쓴 경우가 많습니다.
절대 하지 않는 일, 그리고 그 대가
xsi:schemaLocation을 가져오는 일은 절대 없습니다. XSD 1부는 이 속성을 "스키마 문서의 물리적 위치에 대한" 힌트라고 부르며, 프로세서는 "예컨대 호출하는 애플리케이션이 달리 지시하지 않는 한 역참조를 시도해야 한다"고 말합니다. 가져오지 않는 것은 규격에 맞는 동작이며 흔한 일이기도 합니다. SQL Server는 xml 타입 열에서 이를 무시합니다.
원한다 해도 아무것도 가져올 수 없습니다. 모든 파싱이 XML_PARSE_NO_XXE, NONET, NO_SYS_CATALOG를 쓰고, 이 빌드는 리소스 로더를 등록하지 않으므로 외부 엔티티도, 외부 DTD 서브셋도, schemaLocation URL도 전부 아무것으로도 해석되지 않습니다. XML_PARSE_HUGE는 절대 켜지 않으므로 libxml2의 상한이 그대로 유지됩니다. 요소 깊이 256, 엔티티 중첩 20, 그리고 증폭 한도입니다.
솔직히 밝힐 약점이 하나 있습니다. libxml2 문서는 자신의 스키마 모듈을 XML Schema 1부의 부분 구현이라고 부르며, 같은 입력에 대해 Xerces보다 진단을 적게 내놓는 경우가 많습니다. 목록은 "적어도 이만큼"으로 읽고, 하나씩 고칠 때마다 다시 실행하세요.
코드에서 XSD로 검증하기
XML을 다루는 언어들에서의 같은 검사입니다. 모든 예제가 외부 접근을 끕니다. 스키마 참조는 곧 "가져오라"는 지시이고, 이 라이브러리 대부분이 기본적으로 그것을 따르기 때문입니다.
// npm i libxml2-wasm, the same engine this page runs.
import { XmlDocument, XsdValidator, ParseOption, XmlValidateError } from 'libxml2-wasm';
// NO_XXE blocks external DTDs and entities. Leaving HUGE unset is what keeps
// libxml2's depth, entity-nesting and amplification limits switched on.
const SAFE = ParseOption.XML_PARSE_NO_XXE | ParseOption.XML_PARSE_NONET;
export function validateXsd(xmlText, xsdText) {
let xsd = null;
let validator = null;
let doc = null;
try {
xsd = XmlDocument.fromString(xsdText, { option: SAFE });
validator = XsdValidator.fromDoc(xsd);
doc = XmlDocument.fromString(xmlText, { option: SAFE });
validator.validate(doc);
return { valid: true, errors: [] };
} catch (err) {
if (err instanceof XmlValidateError) {
// Each detail is { line, col, level, message, xpath }.
return { valid: false, errors: err.details };
}
throw err;
} finally {
// dispose() is mandatory: these are WebAssembly heap allocations.
doc?.dispose();
validator?.dispose();
xsd?.dispose();
}
}# pip install lxml
from lxml import etree
parser = etree.XMLParser(
resolve_entities=False, # no entity substitution
no_network=True, # never fetch a schema or DTD
load_dtd=False,
huge_tree=False, # keep the depth and expansion limits
)
schema = etree.XMLSchema(etree.parse('schema.xsd', parser))
doc = etree.parse('document.xml', parser)
if schema.validate(doc):
print('valid')
else:
for e in schema.error_log:
print(f'{e.line}:{e.column} {e.message}')
# lxml wraps libxml2, so this is XSD 1.0. For xs:assert and xs:alternative use
# the pure-Python implementation instead:
# import xmlschema
# xmlschema.XMLSchema11('schema.xsd').validate('document.xml')import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
// With Xerces on the classpath, XSD 1.1 is one string away:
// SchemaFactory.newInstance("http://www.w3.org/XML/XMLSchema/v1.1");
// "file" allows xs:import of a schema next to this one; http is refused.
// Pass "" instead to block every external reference, local ones included.
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file");
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
Schema schema = factory.newSchema(new StreamSource(new java.io.File("schema.xsd")));
Validator validator = schema.newValidator();
validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
// Without an ErrorHandler the first error throws and you see one problem.
// With one, Xerces carries on and reports the lot.
validator.setErrorHandler(new DefaultHandler() {
@Override public void error(SAXParseException e) {
System.out.printf("%d:%d %s%n", e.getLineNumber(), e.getColumnNumber(), e.getMessage());
}
@Override public void fatalError(SAXParseException e) throws SAXParseException {
throw e;
}
});
validator.validate(new StreamSource(new java.io.File("document.xml")));using System;
using System.Xml;
using System.Xml.Schema;
var schemaSettings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null, // no xs:import over the network
};
var schemas = new XmlSchemaSet { XmlResolver = null };
using (var schemaReader = XmlReader.Create("schema.xsd", schemaSettings))
{
schemas.Add(null, schemaReader); // null: take targetNamespace from the file
}
var settings = new XmlReaderSettings
{
ValidationType = ValidationType.Schema,
Schemas = schemas,
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
};
// Never follow xsi:schemaLocation from the instance document.
settings.ValidationFlags &= ~XmlSchemaValidationFlags.ProcessSchemaLocation;
// Without a handler the first error throws; with one, validation continues.
settings.ValidationEventHandler += (_, e) =>
Console.WriteLine($"{e.Exception.LineNumber}:{e.Exception.LinePosition} {e.Message}");
using var reader = XmlReader.Create("document.xml", settings);
while (reader.Read()) { } // validation happens as the document is read
// System.Xml.Schema is XSD 1.0, and Microsoft has said it is not moving past it.<?php
// DOMDocument is libxml2, so this is the same engine and the same XSD 1.0.
libxml_use_internal_errors(true);
$doc = new DOMDocument();
if (!$doc->loadXML($source, LIBXML_NONET)) {
fwrite(STDERR, "The document is not well-formed.\n");
exit(1);
}
// schemaValidateSource() takes the XSD as a string if you already have it.
if (!$doc->schemaValidate('schema.xsd')) {
foreach (libxml_get_errors() as $e) {
fprintf(STDERR, "%d:%d %s\n", $e->line, $e->column, trim($e->message));
}
libxml_clear_errors();
exit(1);
}
echo "valid\n";# xmllint ships with libxml2 and is the same engine as this page.
# --nonet stops it fetching anything the document or the schema references.
xmllint --noout --nonet --schema schema.xsd document.xml
# Exit status is 0 when the document is valid, non-zero otherwise, and the
# diagnostics go to stderr in file:line: form.
# A schema split across xs:import files works here, because xmllint reads the
# sibling files from disk. That is the one thing a browser cannot do.
# No widely installed command-line tool does XSD 1.1; for that you need
# Xerces-J or Saxon-EE, driven from code.Java와 C# 예제의 핸들러 패턴을 보세요. 두 API 모두 핸들러를 설치하지 않으면 첫 오류에서 예외를 던집니다. 운영 코드가 한 번에 문제 하나만 보고하는 이유가 대개 이것입니다.
자주 묻는 질문
제 XML과 XSD가 어딘가로 업로드되나요?
아닙니다. 두 창 모두 이 탭 안에 머무릅니다. libxml2는 여기서 WebAssembly로 Web Worker 안에서 실행되고, 업로드 엔드포인트는 없으며, 이 빌드에는 자체 네트워크 경로가 없습니다.
이 페이지에서는 그 점이 다른 페이지보다 더 중요합니다. 스키마는 모든 필드와 코드 목록, 내부 경계에 이름을 붙인 문서이고, 정작 XML 쪽이 테스트 데이터일 때조차 스키마는 비밀유지 대상인 경우가 많습니다. 네트워크 패널을 열고 검증을 누른 뒤, 엔진이 한 번 로드되고 그 뒤로는 아무 일도 없는 것을 확인해 보세요. 두 창 모두 새로고침에 대비해 localStorage에 저장되며, 300 KB가 넘는 것은 아예 저장하지 않습니다.
XSD 1.1이나 xs:assert를 지원하나요?
아닙니다. libxml2는 XSD 1.0이라서 xs:assert, xs:alternative, xs:openContent, xs:override는 컴파일되지 않습니다. 알 수 없는 "element assert is not expected here" 대신, 패널은 스키마가 적격 형식의 XML이지만 쓸 수 있는 XSD는 아니라고 말하고 1.1을 유력한 원인으로 지목합니다.
1.1이 필요하면 네임스페이스 문자열 하나만 바꾸면 1.1 SchemaFactory를 주는 무료 Xerces-J, 또는 Saxon-EE나 파이썬 xmlschema 패키지를 쓰세요. 브라우저 기반으로는 어떤 것도 지원하지 않습니다. 전부 libxml2 빌드이기 때문입니다.
제 스키마가 xs:import를 씁니다. 여기서도 검증할 수 있나요?
먼저 하나로 합쳤을 때만 가능합니다. xs:import를 해석한다는 것은 파일을 읽거나 요청을 보낸다는 뜻인데, 이 빌드는 리소스 로더를 등록하지 않습니다. 외부 엔티티를 가져오지 않는 것과 같은 결정이며, 그래서 스키마가 컴파일되지 않습니다.
가져오는 타입 정의를 검증에 쓰는 문서 안에 붙여 넣거나, 로컬에서 xmllint를 돌리세요. 로컬에서는 옆에 있는 .xsd 파일들을 디스크에서 읽습니다. UBL과 FpML을 비롯한 큰 공개 스키마 모음 대부분이 이런 처리를 필요로 합니다.
cvc-complex-type.2.4.a는 무슨 뜻인가요?
스키마가 허용하지 않은 자리에 요소가 나타났다는 뜻입니다. 메시지 끝에는 그 자리에서 허용되던 이름들이 중괄호 안에 나옵니다. "Invalid content was found starting with element 'item'. One of '{qty}' is expected."라면 다음 차례가 qty였다는 뜻입니다.
원인은 거의 세 가지입니다. xs:sequence에 대해 자식의 순서가 틀렸거나(압도적으로 흔함), 요소가 아예 선언되지 않았거나(대개 오타이거나 연동의 한쪽이 추가한 필드), 이름은 맞는데 네임스페이스가 틀렸거나. 마지막 경우는 메시지가 지역 이름만 출력하므로 보여 줄 수 없습니다.
xsi:schemaLocation에 적힌 스키마를 가져오나요?
절대 아닙니다. 명세는 이 속성을 힌트라고 부르며, 호출하는 애플리케이션이 프로세서에게 역참조하지 말라고 지시할 수 있게 허용합니다. 그러니 가져오지 않는 것은 편법이 아니라 규격에 맞는 동작입니다.
이유는 개인정보에 그치지 않습니다. 신뢰할 수 없는 문서에 적힌, 공격자가 통제하는 URL을 따라가는 것은 요청 위조의 발판이며, 검증 결과를 "오늘 그 제3자가 내주는 것"에 묶어 버리는 일이기도 합니다.
얼마나 큰 문서까지 검사할 수 있나요?
20메가바이트입니다. 그보다 큰 파일은 불러오기를 거절합니다. 모든 것이 이 페이지의 메모리에 올라가고, 검증은 전체 트리에 더해 컴파일된 스키마까지 만들기 때문입니다.
정말 큰 문서라면 로컬에서 xmllint를 돌리세요. 같은 libxml2 코드를 브라우저의 상한 없이 쓸 수 있습니다.