Лирическое вступление

Идея создания нового языка пришла мне в голову, когда я получил задачу описать ТЗ для подрядчика на разработку 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)


  1. martyncev
    19.02.2026 06:40

    Идея создания нового языка пришла мне в голову, когда я получил задачу описать ТЗ для подрядчика на разработку API. Передо мной встал вопрос: как легко и понятно описать требования к контрактам?

    OpenAPI прекрасно читается и некоторые даже код по нему генерят


    1. Reposlav Автор
      19.02.2026 06:40

      Если говорить именно о моей задаче описания ТЗ, то да, OpenApi вполне подходил. В том случае я не стал его использовать из-за моего личного к нему отношения. Я не считаю его читабельным.

      Если говорить в общем о сравнении OpenApi и Scedel, то:

      • OpenApi протоколо-центричный. Это прекрасно для описания REST-апи, однако ограничивает его использование;

      • OpenApi использует YAML, что с одной стороны хорошо, так как YAML - это устоявшийся формат, с которым умеют работать все системы. С другой стороны, из-за подчинения синтаксису YAML, OpenApi становится очень объемным в описании сложных схем (такая же проблема, как и в JSON Schema)


  1. nin-jin
    19.02.2026 06:40

    Зачем разделение на типы и валидаторы? Как из нескольких валидаторов собрать один? Почему параметры валидаторов то в квадратных то в круглых скобках?


    1. Reposlav Автор
      19.02.2026 06:40

      Невстроенные валидаторы нужны для доменно-специфичных вещей и переиспользования. Валидаторы сами по себе могут быть не очень читаемыми, и совмещение типа и валидатора вместе может вызывать сильную когнитивную нагрузку при чтении.

      Несколько валидаторов в один сами по себе не собираются. Валидатор присоединяется к типу как констрейнт. Можно сказать, что пользовательский тип - это базовый тип + констрейнты. На тип можно навесить несколько констрейнтов:

      validator String(alphaOnly) = this matches /[a-zA-z]+/
      type Username = String(alphaOnly, min:3, max:10)

      Здесь я навесил на тип три констрейнта (один пользовательский и два встроенных), и, для удобства использования, дал этому сложному типу алиас


      1. nin-jin
        19.02.2026 06:40

        Как к этому алиасу добавить еще один констрейнт?


        1. Reposlav Автор
          19.02.2026 06:40

          Это хороший вопрос, спасибо!

          Изначально я планировал, что создавать валидаторы можно только для базовых простых скалярных типов.

          Теоретически, можно было бы сделать так:

          Username(anotherConstraint)

          Но не уверен, как сейчас отреагируют на такое библиотеки, и это не описано в RFC.

          Надо подумать, стоит ли давать возможность навешивать констрейнты на типы, производные от базовых. Здесь вопрос в том, не усложнит ли это интерпретацию кода пользователем


    1. 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]
      }


  1. milinsky
    19.02.2026 06:40

    Первым делом я подумал о JSON Schema, однако из-за её многословности я решил отказаться от неё, на мой взгляд, она недостаточно человекочитаема.

    Интересный проект. Однако я год назад подумывал создать свой DSL, и проблема таких кастомных решений - это сложность их последующего расширения если вдруг понадобится использовать его для более широких целей, и такие языки к сожалению, становятся заложниками узких кейсов применения. Что касается JSON Schema то она и не призвана быть малословной, а скорее наоборот, многословность обусловлена высоким охватом практически любого кейса. И я не знаю какой кейс JSON Schema неспособна покрыть при должном знании спецификации. Разве что GraphQL и gRPC (но это отдельная история). Для удобства чтения - есть Swagger UI который идеально подходит для чтения.

    Но, тем не менее, желаю успехов!


    1. Reposlav Автор
      19.02.2026 06:40

      Спасибо!

      Отмечу, что JSON Schema и Scedel не совсем конкуренты.

      JSON Schema - это, в первую очередь, язык описания правил валидации.

      Scedel - это язык для моделирования типов и контрактов, а валидация - это побочное свойство.