Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 22 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![Total Downloads](https://poser.pugx.org/spiral/json-schema-generator/downloads)](https://packagist.org/packages/spiral/json-schema-generator)
[![psalm-level](https://shepherd.dev/github/spiral/json-schema-generator/level.svg)](https://shepherd.dev/github/spiral/json-schema-generator)

The JSON Schema Generator is a PHP package that simplifies the generation of [JSON schemas](https://json-schema.org/) from Data Transfer Object (DTO) classes.
The JSON Schema Generator is a PHP package that simplifies the generation of [JSON schemas](https://json-schema.org/) from Data Transfer Object (DTO) classes.
It supports PHP enumerations and generic type annotations for arrays and provides an attribute for specifying title, description, and default value.

Main use case - structured output definition for LLMs.
Expand Down Expand Up @@ -107,39 +107,22 @@ Example array output:
'description' => [
'title' => 'Description',
'description' => 'The description of the movie',
'type' => 'string',
'type' => ['string', 'null'],
],
'director' => [
'type' => 'string',
'type' => ['string', 'null'],
],
'releaseStatus' => [
'title' => 'Release Status',
'description' => 'The release status of the movie',
'allOf' => [
[
'$ref' => '#/definitions/ReleaseStatus',
],
],
'type' => ['string', 'null']
'enum' => ['Released', 'Rumored', 'Post Production', 'In Production', 'Planned', 'Canceled'],
],
],
'required' => [
'title',
'year',
],
'definitions' => [
'ReleaseStatus' => [
'title' => 'ReleaseStatus',
'type' => 'string',
'enum' => [
'Released',
'Rumored',
'Post Production',
'In Production',
'Planned',
'Canceled',
],
],
],
];
```

Expand All @@ -160,6 +143,7 @@ final class Actor
* @var array<Movie>
*/
public readonly array $movies = [],
public readonly ?Movie $bestMovie = null;
) {
}
}
Expand Down Expand Up @@ -197,6 +181,18 @@ Example array output:
],
'default' => [],
],
'bestMovie' => [
'title' => 'Best Movie',
'description' => 'The best movie of the actor',
'oneOf' => [
[
'$ref' => '#/definitions/Movie',
],
[
'type' => 'null',
],
],
],
],
'required' => [
'name',
Expand All @@ -219,38 +215,23 @@ Example array output:
'description' => [
'title' => 'Description',
'description' => 'The description of the movie',
'type' => 'string',
'type' => ['string', 'null'],
],
'director' => [
'type' => 'string',
'type' => ['string', 'null'],
],
'releaseStatus' => [
'title' => 'Release Status',
'description' => 'The release status of the movie',
'allOf' => [
[
'$ref' => '#/definitions/ReleaseStatus',
],
],
'type' => ['string', 'null']
'enum' => ['Released', 'Rumored', 'Post Production', 'In Production', 'Planned', 'Canceled'],
],
],
'required' => [
'title',
'year',
],
],
'ReleaseStatus' => [
'title' => 'ReleaseStatus',
'type' => 'string',
'enum' => [
'Released',
'Rumored',
'Post Production',
'In Production',
'Planned',
'Canceled',
],
]
],
];
```
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
],
"require": {
"php": ">=8.1",
"symfony/property-info": "^6.4.18 || ^7.2",
"symfony/property-info": "^6.4.18 || ^7.2.0 <7.3",
"phpstan/phpdoc-parser": "^1.33 | ^2.1",
"phpdocumentor/reflection-docblock": "^5.3"
},
Expand Down
33 changes: 22 additions & 11 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,6 @@ public function generate(string|\ReflectionClass $class): Schema
protected function generateDefinition(ClassParserInterface $class, array &$dependencies = []): ?Definition
{
$properties = [];
if ($class->isEnum()) {
return new Definition(
type: $class->getName(),
options: $class->getEnumValues(),
title: $class->getShortName(),
);
}

// class properties
foreach ($class->getProperties() as $property) {
$psc = $this->generateProperty($property);
Expand Down Expand Up @@ -137,14 +129,33 @@ protected function generateProperty(PropertyInterface $property): ?Property

$required = $default === null && !$type->allowsNull();
if ($type->isBuiltin()) {
return new Property($type->getName(), $options, $title, $description, $required, $default, $format);
return new Property(
type: $type->getName(),
options: $options,
title: $title,
description: $description,
required: $required,
allowsNull: $type->allowsNull(),
default: $default,
enum: $type->getEnumValues(),
format: $format,
);
}

// Class or enum
// Class
$class = $type->getName();

return \is_string($class) && \class_exists($class)
? new Property($class, [], $title, $description, $required, $default, $format)
? new Property(
type: $class,
options: [],
title: $title,
description: $description,
required: $required,
allowsNull: $type->allowsNull(),
default: $default,
format: $format,
)
: null;
}
}
51 changes: 41 additions & 10 deletions src/Parser/ClassParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ public function getProperties(): array

$properties[] = new Property(
property: $property,
type: new Type(name: $type->getName(), builtin: $type->isBuiltin(), nullable: $type->allowsNull()),
type: new Type(
name: $this->getTypeName($type),
builtin: $this->getTypeBuildIn($type),
nullable: $type->allowsNull(),
enum: $this->getEnumValues($type),
),
hasDefaultValue: $this->hasPropertyDefaultValue($property),
defaultValue: $this->getPropertyDefaultValue($property),
collectionValueTypes: $this->getPropertyCollectionTypes($property->getName()),
Expand All @@ -103,21 +108,47 @@ public function isEnum(): bool
return $this->class->isEnum();
}

public function getEnumValues(): array
private function getEnumValues(\ReflectionNamedType $type): ?array
{
if (!$this->isEnum()) {
throw new GeneratorException(\sprintf('Class `%s` is not an enum.', $this->class->getName()));
if (!\is_subclass_of($type->getName(), \BackedEnum::class)) {
return null;
}

$values = [];
foreach ($this->class->getReflectionConstants() as $constant) {
$value = $constant->getValue();
\assert($value instanceof \BackedEnum);
$reflectionEnum = new \ReflectionEnum($type->getName());

$values[] = $value->value;
return \array_map(
static fn(\ReflectionEnumUnitCase $case): int|string => $case->getValue()->value,
$reflectionEnum->getCases(),
);
}

private function getTypeBuildIn(\ReflectionNamedType $type): bool
{
if ($type->isBuiltin() || \is_subclass_of($type->getName(), \BackedEnum::class)) {
return true;
}

return false;
}

/**
* @return non-empty-string
*/
private function getTypeName(\ReflectionNamedType $type): string
{
$typeName = $type->getName();
if ($type->isBuiltin() || !\is_subclass_of($typeName, \BackedEnum::class)) {
return $typeName;
}

$reflection = new \ReflectionEnum($typeName);
$backingType = $reflection->getBackingType();

if (!$backingType instanceof \ReflectionNamedType) {
return $typeName;
}

return $values;
return $backingType->getName();
}

/**
Expand Down
4 changes: 0 additions & 4 deletions src/Parser/ClassParserInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,4 @@ public function getShortName(): string;
* @return array<PropertyInterface>
*/
public function getProperties(): array;

public function isEnum(): bool;

public function getEnumValues(): array;
}
11 changes: 11 additions & 0 deletions src/Parser/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public function __construct(
string $name,
private readonly bool $builtin,
private readonly bool $nullable,
private readonly ?array $enum = null,
) {
/** @psalm-suppress PropertyTypeCoercion */
$this->name = $this->builtin ? SchemaType::fromBuiltIn($name) : $name;
Expand All @@ -45,4 +46,14 @@ public function allowsNull(): bool
{
return $this->nullable;
}

public function isEnum(): bool
{
return $this->enum !== null;
}

public function getEnumValues(): ?array
{
return $this->enum;
}
}
4 changes: 4 additions & 0 deletions src/Parser/TypeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ public function getName(): string|SchemaType;
public function isBuiltin(): bool;

public function allowsNull(): bool;

public function isEnum(): bool;

public function getEnumValues(): ?array;
}
16 changes: 14 additions & 2 deletions src/Schema/Property.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ public function __construct(
public readonly string $title = '',
public readonly string $description = '',
public readonly bool $required = false,
public readonly bool $allowsNull = false,
public readonly mixed $default = null,
public ?array $enum = null,
public readonly ?Format $format = null,
) {
if (\is_string($this->type) && !\class_exists($this->type)) {
Expand Down Expand Up @@ -57,11 +59,21 @@ public function jsonSerialize(): array

if (\is_string($this->type)) {
// this is nested class
$property['allOf'][] = ['$ref' => (new Reference($this->type))->jsonSerialize()];
if ($this->allowsNull) {
$property['oneOf'][] = ['$ref' => (new Reference($this->type))->jsonSerialize()];
$property['oneOf'][] = ['type' => Type::Null->value];
return $property;
}
$property['$ref'] = (new Reference($this->type))->jsonSerialize();

return $property;
}

$property['type'] = $this->type->value;
$property['type'] = $this->allowsNull ? [$this->type->value, Type::Null->value] : $this->type->value;

if ($this->enum !== null) {
$property['enum'] = $this->enum;
}

if ($this->type === Type::Array) {
if (\count($this->options) === 1) {
Expand Down
Loading
Loading