Лирическое вступление
Идея создания нового языка пришла мне в голову, когда я получил задачу описать ТЗ для подрядчика на разработку API. Передо мной встал вопрос: как легко и понятно описать требования к контрактам? Первым делом я подумал о JSON Schema, однако из-за её многословности я решил отказаться от неё, на мой взгляд, она недостаточно человекочитаема. Я перебрал еще варианты, которые могли помочь мне решить проблему: Proto, Typescript, Cue, даже об SQL подумал. Все немного не подходило под мою задачу. В итоге, я остановился на описании контракта на Typescript, и уточнении требований в комментариях.
Тогда-то мне и пришла в голову мысль разработать подходящий инструмент. Это наложилось на моё желание попрактиковаться в написании языков, и я взялся за дело.
Принципы и требования
Для начала, я составил список того, для чего будет использоваться и каким принципам будет следовать новый язык. Я остановился на том, что главной задачей будет создание единой точки правды для контрактов взаимодействия между различными системами. На основе описанной схемы можно валидировать данные, генерировать код, а также расширять одни схемы другими.
Основными же принципами я выделил следующее:
Человекочитаемость и лаконичность
Описание схемы должно быть легко прочитано человеком, причём не только программистом, но человеком смежных профессий: аналитиком, PO, PM.
Language- & protocol-agnostic
Язык не должен указывать на то, какие технологии будут использоваться для реализации контракта.
Расширяемость и переиспользуемость
Необходимо давать возможность расширять правила своими собственными и дать возможность опосредованно управлять процессом валидации и генерации.
Собственный синтаксис
Хотя я взял за основу Typescript, как язык с очень богатым функционалом описания типов, решил отказаться от совместимости с каким-либо языком, так как это могло повредить читаемости и лаконичности.
Структура описания схемы
Валидатор
Валидатор - это правило проверки соответствия данных схеме. Как пример, вот описание валидатора для типа String под названием alphaOnly
validator String(alphaOnly) = this matches /[a-zA-z]+/
А вот более полное описание для валидатора, запрещающего слова, в которых больше i прописных букв подряд:
validator String(noCaps, i=2) = { rule: not this matches /.*[A-Z]{$i,}.*/ message: 'Must not have CAPS words with length more than $i' }
Тип
Тип - описание структуры данных. Типы могут иметь ограничения, основанные на валидаторах.
type Status = 'banned'|'active' type User = { username: String(alphaOnly, min:3, max:10) status: Status }
Подключение других файлов
include "./myDateTime.scedel" type Status = 'banned'|'active' type User = { username: String(alphaOnly, min:3, max:10) status: Status bannedAt: when status = 'banned' then DateTimeFormatted else absent }
Здесь также можно увидеть механизм условных типов и особый absent, который заявляет, что поле должно отсутствовать.
Аннотации
Аннотации позволяют давать инструкции сторонним инструментам. Например, мы можем указать, в какую директорию должен складывать сгенерированные файлы генератор кода. Язык не определяет набор аннотаций, они диктуются сторонним инструментарием.
@php.codegen.namespace = "App\\Entities" @php.codegen.dir = "src/Entities" type Comment = { @js.native.ignore id: Uint text: String @php.symfony.ignore createdAt: DateTimeFormatted? default now() }
Практически весь доступный функционал языка можно увидеть в следующем примере:
scedel-version 1.0 include "https://example.com/remote.scedel" include "./local.scedel" /* * multiline * comment */ validator String(numeric) = this matches /[0-9]+/ validator String(notNumeric) = not this matches /[0-9]+/ validator String(noCaps, i=2) = { rule: not this matches /.*[A-Z]{$i,}.*/ message: 'Must not have CAPS words with length more than $i' } // line comment type AliasType = Int type ConstrainedType = Float(min:10) type UnionType = 'First'|"Second" type NullableType1 = Bool|Null type NullableType2 = ?Bool type NegativeConstrainted = Url(domain: not ["localhost", '127.0.0.1']) type RecordType = { field1: String(max:10) fieldUnion: "Draft"|'Published' conditionalField: when this.recordField.subField2 = 'https://test.com' then UnionType else absent optionalField?: DateTime(format:"YYYY-MM-DD") nullableField: ?IpV6 dependentTypeFieldFrom: DateTime(max: this.dependentTypeFieldTo) dependentTypeFieldTo: DateTime(min: this.dependentTypeFieldFrom) fieldWithCustomValidator: String(noCaps) dictField: dict<String(max:10), String(max:255)> recordField: { subField1: Binary, subField2: Url(scheme:'https') } } type ArrayType = Ushort[min:1, max:3] type ComplexType = Email(domain:'gmail.com')[min:1]|False type IntersectType = RecordType & {additionalField: Url} @symfony.codegen.dir='src/Entities' @reactjs.codegen.dir='Entities' type WithAnnotationsType = Uuid @php.codegen.namespace='\\App\\Entities' on WithAnnotationsType
Конкретный пример
Давайте посмотрим, как Scedel может использоваться в реальности.
Предположим, Github выложил описание контрактов своего апи на https://api.github.com/contracts/issues.scedel
type GithubUser = { id: Ulong login: String(min:1) } type GithubIssue = { id: Ulong number: Int(min:1) title: String(min:1) state: "open" | "closed" locked: Bool user: GithubUser closed_at: when this.state = "closed" then DateTime else absent }
Вы хотите создать интеграцию своего сервиса с Гитхабом. Для этого вы можете создать собственный .scedel-файл типа:
include "https://api.github.com/contracts/issues.scedel" @php.codegen.symfony.namespace='\\App\\Entities' on GithubUser @php.codegen.symfony.namespace\='\\App\\Entities' on GithubIssue
Получившаяся схема будет вашим источником правды, который вы сможете использовать по своему усмотрению. Например, вы можете запустить генератор кода, который, опираясь на схему и аннотации, создаст набор классов, соответствующих этому контракту. Причем, вы можете аннотировать схему от Гитхаба для разных стеков, например, для бэкенда и фронтенда, и, играя аннотациями, генерировать независимый код для PHP и JS с общей базой.
Что уже есть
Весь существующий материал сейчас находится на Гитхабе https://github.com/ScedelLang
Имеется довольно объемный RFC
https://github.com/ScedelLang/grammar/blob/main/RFC-Scedel-0.14.2.md
ANTLR-грамматика
https://github.com/ScedelLang/grammar/blob/main/Scedel-0.14.2.g4
Плагин для Idea для подсветки синтаксиса
https://github.com/ScedelLang/idea-plugin
И еще несколько пакетов для PHP и JS (на подходе C# и Python)
https://github.com/orgs/ScedelLang/repositories
Для каждого языка есть парсер, построитель репозитория схемы, валидатор JSON и генератор кода.
Всё распространяется под лицензией MIT.
Заключение
Конечно, продукт пока еще сырой (на что намекает текущая мажорная версия RFC 0.14.2), и может быть не готов к использованию в серьёзном продакшене. Грамматика может еще активно меняться. Есть несколько идей по развитию языка, например, глобальные аннотации и неймспейсы. Однако Scedel уже может быть вам полезен.
Буду благодарен за конструктивную критику и приглашаю поучаствовать в развитии проекта.
Комментарии (9)

nin-jin
19.02.2026 06:40Зачем разделение на типы и валидаторы? Как из нескольких валидаторов собрать один? Почему параметры валидаторов то в квадратных то в круглых скобках?

Reposlav Автор
19.02.2026 06:40Невстроенные валидаторы нужны для доменно-специфичных вещей и переиспользования. Валидаторы сами по себе могут быть не очень читаемыми, и совмещение типа и валидатора вместе может вызывать сильную когнитивную нагрузку при чтении.
Несколько валидаторов в один сами по себе не собираются. Валидатор присоединяется к типу как констрейнт. Можно сказать, что пользовательский тип - это базовый тип + констрейнты. На тип можно навесить несколько констрейнтов:
validator String(alphaOnly) = this matches /[a-zA-z]+/ type Username = String(alphaOnly, min:3, max:10)Здесь я навесил на тип три констрейнта (один пользовательский и два встроенных), и, для удобства использования, дал этому сложному типу алиас

nin-jin
19.02.2026 06:40Как к этому алиасу добавить еще один констрейнт?

Reposlav Автор
19.02.2026 06:40Это хороший вопрос, спасибо!
Изначально я планировал, что создавать валидаторы можно только для базовых простых скалярных типов.
Теоретически, можно было бы сделать так:
Username(anotherConstraint)Но не уверен, как сейчас отреагируют на такое библиотеки, и это не описано в RFC.
Надо подумать, стоит ли давать возможность навешивать констрейнты на типы, производные от базовых. Здесь вопрос в том, не усложнит ли это интерпретацию кода пользователем

Reposlav Автор
19.02.2026 06:40Констрейнты в квадратных скобках - специфичные для массивов ограничения
Email(domain:'gmail.com')[min:1]Здесь описан массив, состоящий из элементов типа
Email(domain:'gmail.com'), в котором должен быть минимум один элемент. Можно написать чуть по-другому, так будет нагляднееtype GmailEmail = Email(domain:'gmail.com') type User = { emailsList: GmailEmail[min:1] }

milinsky
19.02.2026 06:40Первым делом я подумал о JSON Schema, однако из-за её многословности я решил отказаться от неё, на мой взгляд, она недостаточно человекочитаема.
Интересный проект. Однако я год назад подумывал создать свой DSL, и проблема таких кастомных решений - это сложность их последующего расширения если вдруг понадобится использовать его для более широких целей, и такие языки к сожалению, становятся заложниками узких кейсов применения. Что касается JSON Schema то она и не призвана быть малословной, а скорее наоборот, многословность обусловлена высоким охватом практически любого кейса. И я не знаю какой кейс JSON Schema неспособна покрыть при должном знании спецификации. Разве что GraphQL и gRPC (но это отдельная история). Для удобства чтения - есть Swagger UI который идеально подходит для чтения.
Но, тем не менее, желаю успехов!
Reposlav Автор
19.02.2026 06:40Спасибо!
Отмечу, что JSON Schema и Scedel не совсем конкуренты.
JSON Schema - это, в первую очередь, язык описания правил валидации.
Scedel - это язык для моделирования типов и контрактов, а валидация - это побочное свойство.
martyncev
Идея создания нового языка пришла мне в голову, когда я получил задачу описать ТЗ для подрядчика на разработку API. Передо мной встал вопрос: как легко и понятно описать требования к контрактам?OpenAPI прекрасно читается и некоторые даже код по нему генерят
Reposlav Автор
Если говорить именно о моей задаче описания ТЗ, то да, OpenApi вполне подходил. В том случае я не стал его использовать из-за моего личного к нему отношения. Я не считаю его читабельным.
Если говорить в общем о сравнении OpenApi и Scedel, то:
OpenApi протоколо-центричный. Это прекрасно для описания REST-апи, однако ограничивает его использование;
OpenApi использует YAML, что с одной стороны хорошо, так как YAML - это устоявшийся формат, с которым умеют работать все системы. С другой стороны, из-за подчинения синтаксису YAML, OpenApi становится очень объемным в описании сложных схем (такая же проблема, как и в JSON Schema)