JSON Schema의 활용

우동이·2022년 3월 22일
6

JSON Schema

목록 보기
2/2
post-thumbnail

JSON Schema의 활용

  • JSON Schema를 활용하기 위해서는 여러 라이브러리들이 필요합니다.
    • JSON Schema를 활용한 Validator 자동화
      • Ajv 라이브러리
    • 자동 타입 변환 및 생성
      • Json-schema-to-ts 라이브러리
  • 국내에서는 JSON Schema타입스크립트를 활용하는 정보를 찾아볼 수 없었습니다.

Ajv 라이브러리

1. Ajv란?

  • Node.js / 브라우저를 위한 JSON Validator를 생성해주는 라이브러리 입니다.
    • 최신 JSON Type Definition RFC8927 지원 ( Multi Standard 지원 )
  • JSON Schema 공식문서에도 JS를 활용할 때 Ajv 라이브러리를 추천하고 있습니다.
  • JSON Schema를 v8 엔진 최적화를 통해 초고속으로 Validator를 생성합니다.

  • 타입스크립트를 지원하며 타입가드 형태로 활용할 수 있습니다.
  • MIT 라이센스이며 주간 다운로드 수는 약 68,000,000로 엄청난 인기를 받고 있습니다.
    • 약 2달전에 업데이트를 진행하였고 꾸준하게 릴리즈 되고 있습니다.
  • 참고

2. Ajv 사용해보기

  • 설치
npm install ajv

활용 예시 ( 크게 2가지 )

  • JSON Schema / JSON Type Definition

  • Ajv는 스키마를 함수로 컴파일하고 모든 경우를 캐시 합니다.

    • 동일한 스키마 객체가 사용될 때 다시 컴파일 되지 않습니다.
    • 즉 이 기능을 활용하려면 validater 함수를 재사용해야 합니다. ( 권장 )
    • 캐시 활용법
  • JSON Schema 예시

const Ajv = require("ajv");
const ajv = new Ajv();

const schema = {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "integer" },
  },
  required: ["name"],
  additionalProperties: false,
};

// create Validation Function
const validate = ajv.compile(schema);

// Test Success Data
const successed_data = {
  name: "woodong",
  age: 3,
};

// Test Fail Data
const failed_data = {
  name: "woodong",
  age: "3",
};

const valid_1 = validate(successed_data);
const valid_2 = validate(failed_data);

// 검증
if (!valid_1) {
  console.log(validate.errors);
}

if (!valid_2) {
  console.log(validate.errors);
}

// 실패 Log
[
  {
    instancePath: '/age',
    schemaPath: '#/properties/age/type',
    keyword: 'type',
    params: { type: 'integer' },
    message: 'must be integer'
  }
]
  • JSON Type Definition 예시
const Ajv = require("ajv/dist/jtd");
const ajv = new Ajv();

const schema = {
  properties: {
    name: { type: "string" },
  },
  optionalProperties: {
    age: { type: "int32" },
  },
};

const validate = ajv.compile(schema);

const data = {
  name: "woodong",
  age: 28,
};

const valid = validate(data);
if (!valid) console.log(validate.errors);
  • JSON Tpye Definition의 Parsing and Serializing 기능
    • 기존 JSON.stringify( JS 코드를 JSON 문자열로 변환 )보다 10배 이상 빠릅니다.
    • 기존 JSON.parse( JSON 문자열의 구문을 분석하고 JS 객체 형태로 변환)와 속도가 비슷합니다.
      • JSON이 유효하지 않을 경우 JSON.parse보다 더 빨리 실패함으로 더 효율적일 수 있습니다.
      • 빈 스키마일 경우({}) 성능은 JSON.parse가 더 좋습니다.
const Ajv = require("ajv/dist/jtd");
const ajv = new Ajv();

const schema = {
  properties: {
    name: { type: "string" },
  },
  optionalProperties: {
    age: { type: "int32" },
  },
};

// create serialize function
const serialize = ajv.compileSerializer(schema);

const data = {
  name: "woodong",
  age: 28,
};

// result: {"name":"woodong","age":32}
console.log(serialize(data));

// create parse function
const parse = ajv.compileParser(schema);

const json = '{"name": "woodong", "age": 28}';
const invalidJson = '{"unknown": "abc"}';

// result: { name: 'woodong', age: 28 }
console.log(parse(json));
// result: undefind => 예외 처리 제공하지 않음 ( parse 함수 자체 내장 )
console.log(parse(invalidJson));

// parseAndLog 함수 ( 따로 구현 필요 )
function parseAndLog(json) {
  const data = parse(json);
  if (data === undefined) {
    console.log(parse.message); // error message from the last parse call
    console.log(parse.position); // error position in string
  } else {
    console.log(data);
  }
}

// property unknown not allowed
// 11
console.log(parseAndLog(invalidJson));

3. 타입스크립트 적용 ( 미리 선언해야 가능 )

  • tsconfig.json 옵션에 strictNullChecks: true 추가해야함

    • strictNullChecks: 개발자가 null / undefinded값을 참조하는 것을 방지
  • JSON Schema 예시

import Ajv, { JSONSchemaType } from "ajv";
const ajv = new Ajv();

interface userType {
  name: string;
  age?: number;
}

const schema: JSONSchemaType<userType> = {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "integer", nullable: true },
  },
  required: ["name"],
  additionalProperties: false,
};

// create Validation Function
const validate = ajv.compile(schema);

// Test Success Data
const successed_data = {
  name: "woodong",
  age: 3,
};

// Test Fail Data
const failed_data = {
  name: "woodong",
  age: "3",
};

const valid_1 = validate(successed_data);
const valid_2 = validate(failed_data);

// 검증
if (!valid_1) {
  console.log(validate.errors);
}

if (!valid_2) {
  console.log(validate.errors);
}
  • JSON Type Definition 예시
import Ajv, { JTDSchemaType } from "ajv/dist/jtd";
const ajv = new Ajv();

interface userType {
  name: string;
  age?: number;
}

const schema: JTDSchemaType<userType> = {
  properties: {
    name: { type: "string" },
  },
  optionalProperties: {
    age: { type: "int32" },
  },
};

const validate = ajv.compile(schema);

const data = {
  name: "woodong",
  age: 28,
};

const valid = validate(data);
if (!valid) console.log(validate.errors);
  • JSON Type Definition에서 Type 추출
    • 완벽하게 Type / Interface를 생성해주지는 않습니다.
import Ajv, { JTDDataType } from "ajv/dist/jtd";
const ajv = new Ajv();

const schema = {
  properties: {
    name: { type: "string" },
  },
  optionalProperties: {
    age: { type: "int32" },
  },
} as const;

type userType = JTDDataType<typeof schema>;
                            
const validate = ajv.compile<userType>(schema)

/* 예상 Type
type userType2 = {
  name: string;
  age?: number;
}; 
*/
                                                        
/* 변경된 type
type userType = {
    name: string;
} & {
    age?: number | undefined;
} 
*/
  • union 형태 지원
import Ajv, { JSONSchemaType } from "ajv";
const ajv = new Ajv();

type MyUnion = { prop: boolean } | string | number;

const schema: JSONSchemaType<MyUnion> = {
  anyOf: [
    {
      type: "object",
      properties: { prop: { type: "boolean" } },
      required: ["prop"],
    },
    {
      type: ["string", "number"],
    },
  ],
};
  • Generic 적용
    • 공식문서에는 언급하고 있지 않습니다.
    • 개인적으로 적용한 결과 지원하는 것 같습니다.
    • 문제점
      • JSON Shema 특성상 여러가지 Type을 모두 선언할 수 없습니다.
      • Generic Type을 활용해 유동적인 스키마를 만들 수 없습니다.
        • 해결방법을 아직도 못찾는 중...
import Ajv, { JSONSchemaType } from "ajv";
const ajv = new Ajv();

interface userType<T> {
  name: T;
  age?: number;
}

const schema: JSONSchemaType<userType<string>> = {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "integer", nullable: true },
  },
  required: ["name"],
  additionalProperties: false,
};

json-schema-to-ts 라이브러리

1. json-schema-to-ts 라이브러리란?

  • Json Schema로 정의된 문서를 Type 형태로 변환해주는 라이브러리 입니다.
  • 주간 다운로드 수는 약 30,000으로 많이 사용하지 않지만 2달전에 업데이트가 되었으며 꾸준하게 개선하고 있습니다.
    • MIT 라이센스
  • 특히 널리 사용되는 Ajv라이브러리와 같이 사용할 수 있도록 테스트를 완료하였고 타입스크립트의 대부분의 케이스를 지원합니다.
    • 타입스크립티의 모든 최신 버전을 지원하는 것은 아닙니다.
  • 참고

2. 사용해보기

  • 설치
npm i json-schema-to-ts
  • Object 활용
import { FromSchema } from "json-schema-to-ts";

const schema = {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "integer", nullable: true },
  },
  required: ["name"],
  additionalProperties: false,
} as const;

type userType = FromSchema<typeof schema>;
                           
// 생성된 Type
// type userType = {
//   name: string;
//   age?: number | undefined;
// }
  • Enum 활용
    • 비슷하게 type 형태로 구현
const enumSchema = {
  enum: [true, 42, { foo: "bar" }],
} as const;

// => true | 42 | { foo: "bar"}
type Enum = FromSchema<typeof enumSchema>;

JSON Schema와 라이브러리의 활용성

  • 장점
    • Data Model를 정의할 때 타입스크립트보다 더 많은 표현이 가능합니다.
    • 라이브러리를 통해 Validation을 손쉽게 활용할 수 있습니다.
  • 단점
    • JSON Schema 특성상 유동적인 데이터 타입을 지원하지 않기 때문에 라이브러리들이 Generic를 활용하지 않고 있습니다.
      • 여러 타입을 지원해야 할 경우 JSON Schema는 type: ["string", "number"] / TS는 Union을 통해 타입을 제한시켜야 합니다.
    • 만약 JSON Schema를 활용하여 Data Model를 정의한다면 타입이 명확한 Model만 사용해야 합니다.
    • Type을 활용한 Data Model 정의 방식보다는 러닝커브가 훨씬 크고 복잡하며 여러 라이브러리의 도움을 받아야 합니다.
    • 타입스크립트의 Interface 활용도가 떨어집니다.
      • 직접 Interface를 생성해서 활용할 수는 있지만 JSON Schema를 Interface로 변화할 수 없습니다.
profile
아직 나는 취해있을 수 없다...

0개의 댓글