XML에서 XSD 생성하기
출발점이 될 스키마를 추론합니다. 믿기 전에 검토하세요.
모든 처리는 이 탭 안에서 이루어집니다. 붙여넣은 내용은 업로드되거나 기록되거나 전송되지 않습니다. 네트워크 패널을 열어 확인하세요.
예시 문서를 붙여넣으면 그에 맞는 XSD 1.0 골격을 만들어 냅니다. 요소 이름마다 선언 하나, 자식에는 xs:sequence, 자식이나 속성이 있는 곳에는 xs:complexType, 텍스트와 속성을 함께 지닌 요소에는 xs:simpleContent, 그리고 모든 값에 추측한 형. 이 사이트 자체의 스캐너가 웹 워커 안에서 도니, 내려받을 엔진은 없습니다.
협력사가 예시 페이로드만 보내고 스키마는 주지 않았을 때, 코드 생성기에 넣을 무언가가 필요할 때, 또는 지금까지 「상대 시스템이 보내는 것」으로만 존재해 온 형식의 글로 된 설명이 필요할 때 쓰세요.
이것이 다른 점은, 알 수 없는 것을 스스로 말해 준다는 데 있습니다. 문서 하나에서 하는 추론은 그 문서를 설명할 뿐 다른 무엇도 설명하지 않습니다. 어떤 요소가 선택인지도, 실제 값의 범위도, 형식이 허용하는 순서도 알 수 없습니다. 출력은 그 경고를 주석으로 품고 있고, 아래 절들은 어느 부분을 살펴야 하는지 말해 줍니다.
실제로 내놓는 것
문서를 먼저 훑고, 정형식이 아니면 추론을 거부합니다. 그다음 모든 요소를 방문하며 모양을 기록합니다. 어떤 속성이 나타났고 각각이 매번 나타났는지, 어떤 자식이 나타났고 한 부모 아래에 각각 몇 개였는지, 그리고 텍스트가 어떻게 보였는지. 자식도 속성도 없는 요소는 형과 함께 그 자리에 내놓습니다. 자식이 있는 요소에는 예시가 쓴 순서 그대로 sequence를 감싼 complexType을 줍니다. 텍스트와 속성이 있는 요소에는 확장을 붙인 simpleContent를 줍니다.
세 가지 세부가 중요합니다. 모양은 경로가 아니라 요소 이름만으로 색인되므로, <customer> 아래의 <name>과 <product> 아래의 <name>은 하나의 선언으로 합쳐집니다. 자식이 있는 요소가 두 번 나타나면 두 번째는 <xs:element ref="..."/>로 적히는데, XSD는 이것을 전역 선언에 대해서만 해석하므로 선언을 위로 끌어올려야 합니다. 그리고 요소가 텍스트와 자식을 함께 가지면 자식이 이기고 텍스트는 버려집니다. 혼합 내용에는 mixed="true"가 필요하지만, 여기서는 아무것도 그것을 붙이지 않습니다.
형은 어떻게 추측되고, 그 추측은 어디서 틀리는가
형은 예시의 글자에서만 나옵니다. 같은 이름이 서로 달라 보이는 값을 지니면 추측이 넓어집니다. 정수 형태가 섞이면 xs:integer, 정수 옆에 소수가 있으면 xs:decimal, 그 밖의 것은 xs:string으로 물러납니다. 두 번째 출현에서 다른 형처럼 보이는 속성은 xs:string으로 내려갑니다.
실패하는 방식도 거기서 따라옵니다. 「1」과 「2」를 담은 상태 필드는 정수로 형 지정되고, 그러면 스키마는 그 필드가 한 달에 한 번 실어 오는 「N/A」를 거부합니다. 상품 코드 「0123」은 숫자가 되어 앞의 0을 잃고, 「0123」과 「123」이 같은 값이 됩니다. 반대 방향으로 가는 안전장치가 하나 있습니다. 안전한 정수가 되기에는 너무 긴 숫자열, 예컨대 20자리 계좌번호는, 정밀도를 잃는 숫자가 되는 대신 문자열로 남습니다.
- 빈 요소는 xs:string이 됩니다. 아무것도 없는 데서는 아무것도 추론할 수 없기 때문입니다.
- 숫자만이면 xs:nonNegativeInteger, 앞에 빼기 기호가 있으면 xs:integer. 소수점이 있는 숫자는 xs:decimal. 지수 표기는 xs:string으로 떨어집니다.
- 정확히 「true」나 「false」면 xs:boolean. 「1」, 「yes」, 「Y」는 아닙니다.
- YYYY-MM-DD는 xs:date, 같은 것에 T와 시각이 뒤따르면 xs:dateTime. 다른 날짜 형식은 문자열입니다.
- http:// 또는 https://로 시작하는 값은 xs:anyURI. 다른 스킴이나 상대 경로는 아닙니다.
예시 하나로는 알 수 없는 것
가장 큰 빈틈은 출현 횟수입니다. 요소에 minOccurs="0"이 붙는 것은 예시가 그 부재를 보여 준 자리뿐입니다. 뒤쪽 부모 아래에서 처음 나타났거나, 한 번 그것을 가졌던 부모가 이번에는 그것 없이 다시 나타난 경우입니다. 형식에서는 선택이지만 여러분의 예시에서는 줄곧 있었던 필드는 필수로 나오고, 내일 스키마가 적법한 문서를 거부하게 됩니다. maxOccurs도 거칩니다. 한 부모 아래에서 두 번 이상 보인 것은 unbounded가 되므로, 언제나 정확히 둘인 짝이 무제한이 되어 버립니다.
순서는 추론이 아니라 주장입니다. xs:sequence는 자식들이 그 순서로 나타나야 한다고 말하지만, 그것은 예시가 보여 준 것일 뿐 형식이 요구하는 것이라고는 할 수 없습니다. 순서가 상관없다면 xs:all을 쓰세요. XSD 1.0은 내용 모델의 맨 위에서만, 각 요소를 최대 한 번으로 허용합니다. 자식들이 서로 택일이라면 xs:choice를 쓰세요. 범위, 열거, 패턴, xs:key, xs:keyref는 어느 것도 추론되지 않으며, 업무 규칙은 바로 그 안에 있습니다.
이름공간이 가장 날카로운 한계입니다. 요소 이름은 접두사까지 적힌 그대로 가져오므로, <dc:title>을 담은 문서는 <xs:element name="dc:title">을 만들어 냅니다. 그런데 요소 선언의 name은 NCName이어야 하고, NCName에는 콜론이 들어갈 수 없습니다. 그 스키마는 컴파일되지 않습니다.
점검 목록
생성한 스키마가 빌드 파이프라인이나 협력사 근처에 가기 전에 이 목록을 훑으세요. 먼저 출력을 같은 예시에 대해 XSD 검사기로 되돌려 보는 것부터 시작하세요. 자기가 나온 문서를 검증하지 못하는 스키마는 위의 경우 가운데 하나에 걸린 것입니다.
- 모든 minOccurs. 정말로 필수인 필드는 무엇이고, 그저 여러분의 예시에 있었을 뿐인 것은 무엇인가?
- 모든 maxOccurs="unbounded". 실제 상한이 있는가?
- 모든 형. 정수나 논릿값으로 나온 코드·식별자·상태에는 가장 엄격하게.
- xs:sequence. 그것은 xs:all이나 xs:choice여야 하지 않은가?
- 예시가 이름공간을 썼다면 targetNamespace와 요소 이름들.
- 자식과 나란히 텍스트를 지닌 모든 요소에 mixed="true".
- 모든 xs:element ref. 가리킬 전역 선언이 필요합니다.
- 합쳐진 이름들. 두 곳에서 두 가지를 뜻하는 요소 이름에는 두 개의 형이 필요합니다.
- 무엇도 추론할 수 없는 것들. 열거, 패턴, 범위, 키, 키 참조.
코드로 스키마 추론하기
스키마 추론은 .NET의 것을 빼면 어느 표준 라이브러리에도 없어서, 이 도구들은 보통보다 차이가 큽니다. 같은 문서를 모두에게 주면 서로 다른 스키마가 돌아옵니다. 그 차이는 전부 추측에 있습니다.
// No dependency: DOMParser is enough to collect the shape of a document.
// This prints the inventory a schema is built from, which is the part worth
// reading before you trust any generator's output.
function inventory(xmlText) {
const doc = new DOMParser().parseFromString(xmlText, 'application/xml');
if (doc.querySelector('parsererror')) throw new Error('not well-formed');
const shapes = new Map();
const visit = (el) => {
let shape = shapes.get(el.tagName);
if (!shape) {
shape = { count: 0, attrs: new Map(), children: new Map(), values: new Set() };
shapes.set(el.tagName, shape);
}
shape.count++;
for (const a of el.attributes) {
if (a.name === 'xmlns' || a.name.startsWith('xmlns:')) continue;
shape.attrs.set(a.name, (shape.attrs.get(a.name) ?? 0) + 1);
}
const kids = [...el.children];
const seen = new Map();
for (const c of kids) seen.set(c.tagName, (seen.get(c.tagName) ?? 0) + 1);
for (const [name, n] of seen) {
const m = shape.children.get(name) ?? { min: Infinity, max: 0 };
shape.children.set(name, { min: Math.min(m.min, n), max: Math.max(m.max, n) });
}
if (!kids.length && el.textContent.trim()) shape.values.add(el.textContent.trim());
kids.forEach(visit);
};
visit(doc.documentElement);
for (const [name, s] of shapes) {
// An attribute seen fewer times than its element is optional. Present on
// every occurrence proves nothing: it may still be optional in the format.
const optional = [...s.attrs].filter(([, n]) => n < s.count).map(([a]) => a);
console.log(name, 'x' + s.count, 'optional attrs:', optional.join(', ') || 'none');
}
}# pip install defusedxml
# The standard library parser is not safe on input you did not write.
from collections import defaultdict
from defusedxml.ElementTree import parse
def inventory(path):
root = parse(path).getroot()
shapes = defaultdict(lambda: {'count': 0, 'attrs': defaultdict(int),
'children': {}, 'values': set()})
def visit(el):
s = shapes[el.tag]
s['count'] += 1
for name in el.attrib:
s['attrs'][name] += 1
counts = defaultdict(int)
for c in el:
counts[c.tag] += 1
for name, n in counts.items():
lo, hi = s['children'].get(name, (n, n))
s['children'][name] = (min(lo, n), max(hi, n))
if len(el) == 0 and (el.text or '').strip():
s['values'].add(el.text.strip())
for c in el:
visit(c)
visit(root)
for tag, s in shapes.items():
optional = [a for a, n in s['attrs'].items() if n < s['count']]
print(f"{tag} x{s['count']} optional attributes: {optional or 'none'}")
for name, (lo, hi) in s['children'].items():
# lo == 0 is never inferable from a single occurrence of the parent.
print(f" {name}: seen {lo}..{hi} per parent")
inventory('sample.xml')// Apache XMLBeans: org.apache.xmlbeans:xmlbeans:5.2.1
// Inst2Xsd is the closest thing Java has to a standard inference tool, and it
// takes several instance documents, which is the main thing this page cannot.
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.impl.inst2xsd.Inst2Xsd;
import org.apache.xmlbeans.impl.inst2xsd.Inst2XsdOptions;
import org.apache.xmlbeans.impl.xb.xsdschema.SchemaDocument;
import java.io.File;
XmlObject[] instances = new XmlObject[] {
XmlObject.Factory.parse(new File("sample-1.xml")),
XmlObject.Factory.parse(new File("sample-2.xml")), // more samples, better guesses
};
Inst2XsdOptions options = new Inst2XsdOptions();
// RUSSIAN_DOLL nests everything; SALAMI_SLICE makes every element global,
// which is easier to hand-edit afterwards.
options.setDesign(Inst2XsdOptions.DESIGN_SALAMI_SLICE);
options.setSimpleContentTypes(Inst2XsdOptions.SIMPLE_CONTENT_TYPES_SMART);
options.setUseEnumerations(Inst2XsdOptions.ENUMERATION_NEVER);
SchemaDocument[] schemas = Inst2Xsd.inst2xsd(instances, options);
for (int i = 0; i < schemas.length; i++) {
schemas[i].save(new File("inferred-" + i + ".xsd"));
}using System.Xml;
using System.Xml.Schema;
// XmlSchemaInference is in the framework: no package needed. It is also the
// only one of these that will refine an existing schema with a new sample.
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
};
var inference = new XmlSchemaInference
{
// Relaxed: string everywhere. Restricted: guess int, date, boolean and so
// on, with all the risk that implies for codes and identifiers.
TypeInference = XmlSchemaInference.InferenceOption.Restricted,
Occurrence = XmlSchemaInference.InferenceOption.Relaxed,
};
XmlSchemaSet schemas;
using (var reader = XmlReader.Create("sample-1.xml", settings))
{
schemas = inference.InferSchema(reader);
}
using (var reader = XmlReader.Create("sample-2.xml", settings))
{
schemas = inference.InferSchema(reader, schemas); // widen with a second sample
}
using var output = new XmlTextWriter("inferred.xsd", null) { Formatting = Formatting.Indented };
foreach (XmlSchema schema in schemas.Schemas())
{
schema.Write(output);
}# Trang, from the RELAX NG authors, is the best command-line option and takes
# as many samples as you can give it.
java -jar trang.jar -I xml -O xsd sample-1.xml sample-2.xml sample-3.xml inferred.xsd
# It will emit a DTD or a RELAX NG schema from the same input:
java -jar trang.jar -I xml -O dtd sample-1.xml inferred.dtd
# Then do the step most people skip: check the schema against the documents it
# was inferred from before trusting it.
xmllint --noout --nonet --schema inferred.xsd sample-1.xml다른 데서 찾아볼 값어치가 있는 기능은 여러 예시를 받는 것입니다. Trang, XMLBeans, XmlSchemaInference는 모두 문서 여럿을 받아 결과를 넓혀 주므로, 「이 필드는 늘 있었다」가 여러분의 짐작 없이 「이 필드는 때때로 없다」로 바뀝니다.
자주 묻는 질문
스키마를 만들기 위해 제 예시 문서가 업로드되나요?
아닙니다. 추론은 이 사이트 자체의 스캐너에서, 자바스크립트로, 이 탭의 웹 워커 안에서 돕니다. 서버가 관여하지 않으며, 스키마 검사기와 달리 내려받을 WebAssembly 엔진조차 없습니다.
이것은 보기보다 값이 큽니다. 예시란 정의상 실제 페이로드이고, 그 안에는 진짜 고객 이름과 계좌번호와 가격이 들어 있기 때문입니다.
예시 문서를 둘 이상 써서 스키마를 만들 수 있나요?
여기서는 안 됩니다. 이 페이지는 편집기에 있는 문서 하나에서 추론하며, 그것이 한 번 붙여넣어 답하도록 만든 도구의 정직한 한계입니다.
예시가 많아지면 더 나은 스키마가 나옵니다. 어떤 필드가 선택이라는 것을 배울 길은 그것뿐이기 때문입니다. Trang은 입력 파일을 몇 개든 받고, XMLBeans의 Inst2Xsd는 인스턴스 배열을 받고, XmlSchemaInference는 기존 스키마를 예시 하나로 더 다듬습니다. 그렇지 않다면, 가장 큰 예시로 생성한 뒤 minOccurs 값을 손으로 느슨하게 하세요.
우편번호가 왜 정수로 나왔나요?
예시에서 숫자였기 때문이고, 문서 하나 안에는 숫자와 「마침 숫자로 된 코드」를 가려낼 것이 아무것도 없기 때문입니다.
이것이 생성 결과에서 가장 흔히 고치게 되는 것입니다. 우편번호, 상품 코드, 전화번호, 계좌 참조 번호, 그리고 앞에 0이 오는 것은 거의 언제나 xs:string이어야 하고, 때로는 xs:pattern도 함께 두어야 합니다.
제 문서가 이름공간을 쓰는데 스키마가 컴파일되지 않습니다. 이제 어떻게 하나요?
예상된 일입니다. 요소 이름을 적힌 그대로 가져오므로 <dc:title>은 <xs:element name="dc:title">이 되는데, 요소 선언의 name은 NCName이어야 하고 NCName에는 콜론이 들어갈 수 없습니다.
출력은 구조의 목록으로 여기세요. xs:schema 요소에 targetNamespace를 더하고 name 속성에서 접두사를 지우세요. elementFormDefault는 「qualified」로 적혀 있습니다. 자식이 부모와 같은 이름공간에 있다는 뜻입니다.
출력은 XSD 1.0인가요, 1.1인가요?
XSD 1.0이고, 1.1이 추가한 구문은 하나도 쓰지 않습니다. 의도한 것입니다. 1.0은 libxml2, .NET, lxml, xmllint가 모두 구현하는 것이고, 1.1 스키마는 Xerces-J, Saxon-EE, 파이썬 xmlschema 패키지에서 동작하며 그 밖에서는 동작하지 않습니다.
「끝은 시작보다 앞설 수 없다」 같은 필드 사이의 규칙에는 xs:assert가 필요한데, 이것은 1.1에만 있습니다. 그런 규칙은 여러분의 소비자가 처리할 수 있는 판으로, 손으로 더하세요.
생성된 스키마가 쓸 만한지 어떻게 확인하나요?
이 사이트의 XSD 검사기에서 예시를 그 스키마에 비추어 검증하세요. 자기 출처를 검증하지 못하는 스키마는 알려진 경우 가운데 하나에 걸린 것입니다. 이름공간이 붙은 요소 이름, 혼합 내용, 또는 전역이 아닌 선언을 가리키는 ref.
그다음 그 스키마가 한 번도 본 적 없는 문서로, 되도록 다른 날이나 다른 고객의 것으로 시험해 보세요. 잘못된 minOccurs 값이 드러나는 곳이 바로 거기입니다.