#scrambledrop: Scramble 0.13.16

#scrambledrop: Scramble 0.13.16

March 18, 2026

Type inference improvements, extensible rule documentation, clearer validation rules evaluation errors, support for abstract Laravel Data polymorphism, updated lazy properties documentation, and Laravel Actions support in PRO.

Since the last post, Scramble has received many updates. Here are the most prominent changes introduced since the initial 0.13.x release.

Type inference improvements

Automatic type inference is a cornerstone of Scramble’s API documentation generation. The smarter Scramble is at inferring types, the fewer manual annotations you need to get the API documentation you want.

Improved pagination inference

Previously, when writing User::paginate(), Scramble would infer the type LengthAwarePaginator<int, unknown>. That was usually sufficient for API resource collections like UserResource::collection(User::paginate()), since the key information is that the collection is paginated — you don’t need to know the underlying model.

Now Scramble correctly infers LengthAwarePaginator<int, User> for that example. This is useful whether you need to perform operations on your paginator instance or return it directly — you won’t need to add any annotations.

@phpstan-self-out support for vendor classes

A lot of Laravel’s native classes come with extensive annotations, allowing PHPStan to analyze the codebase for errors.

One such annotation is @phpstan-self-out — it documents how (and whether) a class is modified when you call a method on it.

Consider LengthAwarePaginator — the typical way to modify paginated items is by calling through:

$users = User::paginate()->through(fn ($u) => ['id' => $u->id]);

Scramble will now understand that $users has type LengthAwarePaginator<int, array{id: int}>.

In short, thanks to @phpstan-self-out support, Scramble’s type inference is more accurate.

Better array type inference

Scramble is now more accurate when analyzing arrays built by pushing items with $arr[] = $something expressions:

$arr = [];
foreach ($this->counties as $country) {
$arr[] = ['country' => $country];
}
return $arr; // Scramble knows $arr type is array<int, array{country: string}> here

Request documentation improvements

Rules documentation API

In 0.13.3, I added the ability to extend Scramble to document any validation rule: both built-in Laravel rules and your custom ones via the new Rules documentation API.

This lets you document project-specific validation rules without waiting for built-in support.

The API is straightforward: you define how a validation rule affects a field’s JSON Schema. For example, when you use the regex rule on a field, Scramble adds the pattern keyword to the field’s JSON Schema, describing the expected format.

Here’s how you can add ipv4 validation rule support:

use Dedoc\Scramble\Contracts\RuleTransformer;
use Dedoc\Scramble\Support\Generator\Types\Type;
use Dedoc\Scramble\Support\RuleTransforming\NormalizedRule;
use Dedoc\Scramble\Support\RuleTransforming\RuleTransformerContext;
class Ipv4Rule implements RuleTransformer
{
public function shouldHandle(NormalizedRule $rule): bool
{
return $rule->is('ipv4');
}
public function toSchema(Type $previous, NormalizedRule $rule, RuleTransformerContext $context): Type
{
return $previous->format('ipv4');
}
}
// field
'ip' => ['required', 'ipv4']
// JSON Schema
{ "type": "string", "format": "ipv4" }

Learn more about extending Scramble’s validation rules support: https://scramble.dedoc.co/developers/validation-rules

Validation rules evaluation improvements

Version 0.13.15 improves request documentation by handling issues that can occur during validation rule evaluation more gracefully.

You’ll get clearer error messages that make problems easier to debug and fix, plus more resilient rule evaluation that results in more complete request documentation.

For example, the form request below wouldn’t previously be documented because it calls a method on $this->user(). Scramble doesn’t provide a user when generating documentation, so the request wouldn’t be documented at all.

class CreateUserRequest extends FormRequest
{
public function rules(): array
{
return [
'external_id' => [
...$this->someTraitMethod(),
Rule::unique(User::class)
->where('company_id', $this->user()->company_id),
],
];
}
}

Now you’ll get an error that helps you fix the issue (in this case, you can make the method call nullable). If your rules depend on runtime data (authenticated user, request state, database), ensure the code can run without those dependencies, or use the null-safe operator (?->) for those calls.

Cannot evaluate validation rules (2 evaluators failed):
[FormRequestRulesEvaluator] Method CreateUserRequest::someTraitMethod does not exist (at ....)
[NodeRulesEvaluator] Call to undefined method Dedoc\Scramble\Support\OperationExtensions\RulesEvaluator\NodeRulesEvaluator::nonExistingMethod() (at ...)

Smarter lazy Laravel Data properties

Lazy properties in Laravel Data are a powerful way to include data on demand. In the latest releases, Scramble has become much smarter about documenting these properties thanks to the type inference improvements.

Documentation for data classes with lazy properties is now accurate and shows schemas in the way they’ll appear in the actual response.

Consider this example:

// Data class
class UserData extends Data
{
public function __construct(
public int $id,
public Lazy|string $email,
) {}
public static function fromModel(User $model)
{
return new self(
$model->id,
Lazy::create(fn () => $model->email),
);
}
}
// Controller
class UserController
{
public function index()
{
return UserData::collect(User::paginate());
}
}

When you call this endpoint, you’ll get a paginated response containing an array of UserData objects with only the id property in data. Previously, Scramble documented both id and email, marking email as optional.

Now, thanks to the inference improvements, Scramble knows that because we’re passing a paginated User model collection, fromModel will be invoked when creating data objects. As a result, only id will be present in the response (since email is DefaultLazy and not included by default).

If you want to include email, you can either use DataCollection or include it in the through method:

// Controller
class UserController
{
public function index()
{
return UserData::collect(User::paginate(), DataCollection::class)
->include('email');
// or
return UserData::collect(User::paginate())
->through(fn ($u) => $u->include('email'));
}
}

Both approaches will make email appear in the response and in the API documentation.

Learn more about documenting lazy properties: http://scramble.dedoc.co/packages/laravel-data#lazy-property-types-and-schema-accuracy

Abstract Laravel Data classes support

Laravel Data supports abstract data classes that use a property to distinguish between the implementations.

For example, imagine you have a NotificationData class:

namespace App\Data;
use Spatie\LaravelData\Data;
class NotificationData extends Data
{
public function __construct(
public string $id,
) {}
}

You might have multiple notification types: “campaign requested” and “campaign finished”.

If you want to render these notifications, you may want to add additional context for each one. This is where abstract data classes come in handy. Let’s update the notification data class:

namespace App\Data;
use Spatie\LaravelData\Data;
class NotificationData extends Data
{
public function __construct(
public string $id,
public AbstractNotificationContextData $context,
) {}
}

Now, let’s implement notification context data classes:

namespace App\Data;
use Spatie\LaravelData\Attributes\PropertyForMorph;
use Spatie\LaravelData\Contracts\PropertyMorphableData;
use Spatie\LaravelData\Data;
abstract class AbstractNotificationContextData extends Data implements PropertyMorphableData
{
#[PropertyForMorph]
public string $type;
public static function morph(array $properties): ?string
{
return match ($properties['type']) {
'campaign-requested' => CampaignRequestedContextData::class,
'campaign-finished' => CampaignFinishedContextData::class,
default => null
};
}
}
class CampaignRequestedContextData extends AbstractNotificationContextData
{
public function __construct(
public string $campaignName,
public int $rate,
) {}
}
class CampaignFinishedContextData extends AbstractNotificationContextData
{
public function __construct(
public string $campaignName,
public int $completedActions,
public float $finalCpa,
) {}
}

Now, every type of notification will have its own context.

Here’s how Scramble will document such notification data:

Abstract Laravel Data documentation

Notice how Scramble was also able to figure out that the type property must be 'campaign-requested' for CampaignRequestedContextData (and for the other context data classes as well). This deserves its own blog post, but for now I’ll just highlight this attention to detail.

Laravel Actions support

lorisleiva/laravel-actions is a package that lets you encapsulate a single piece of logic in a class — an action. Actions can be used as controllers, with validation rules, authorization logic, and more.

Scramble PRO now supports Laravel Actions, so you get accurate API documentation when using this package.

Other notable changes

  • Added #[Hidden] attribute and @hidden PHPDoc annotation support for hiding object properties from the documentation.
  • Added @deprecated annotation support.
  • Laravel Query Builder 7.0 support.
  • Support for classes that implement the Arrayable interface.
  • And many more: https://scramble.dedoc.co/releases
Scramble PRO
Comprehensive API documentation generation for Spatie’s Laravel Data, Laravel Query Builder, and other packages.