Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Vich Uploader Serialization Bundle Laravel Package

fresh/vich-uploader-serialization-bundle

Symfony bundle that integrates VichUploaderBundle with the Symfony Serializer, exposing uploaded file URLs and metadata in JSON API responses. It removes the need for custom normalizers when serializing entities with file fields. Lightweight and easy to configure for API Platform and Symfony APIs.

View on GitHub
Deep Wiki
Context7

Getting Started

This bundle is a small bridge: it teaches JMSSerializer how to turn a serialized payload (typically a base64 string) into a Symfony\Component\HttpFoundation\File\UploadedFile, so VichUploaderBundle's normal upload lifecycle can take over from there. If you already expose REST/JSON APIs and want clients to send files as JSON instead of multipart/form-data, this is the missing piece.

Minimal setup

composer require fresh/vich-uploader-serialization-bundle:^3.3

v3.3.0 is a dependency-only release: it widens the constraints so the bundle installs cleanly on Symfony 6. There are no new features, config keys, or BC breaks versus 3.2.x — the handler code path is unchanged. Practically, this means you can now add it to a Symfony 6 application without --with-all-dependencies gymnastics or a conflict workaround; on Symfony 5.x the older 3.2 line still works fine.

Register it alongside the two bundles it depends on (order matters only in that both must be present):

// config/bundles.php
return [
    Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
    JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true],
    Fresh\VichUploaderSerializationBundle\VichUploaderSerializationBundle::class => ['all' => true],
];

Confirm the serializer handler is actually registered before writing any code — most "it doesn't work" reports start here:

bin/console cache:clear
bin/console debug:container --tag=jms_serializer.subscribing_handler

You should see a handler from the Fresh\VichUploaderSerializationBundle namespace covering UploadedFile. The bundle exposes a serializer type alias (used as vich_upload in its published examples) that you attach to the uploadable property.

First use case: JSON create endpoint

use JMS\Serializer\Annotation as Serializer;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
 * @Vich\Uploadable
 */
class Product
{
    /**
     * @Vich\UploadableField(mapping="product_image", fileNameProperty="imageName")
     * @Serializer\Type("vich_upload")
     * @Serializer\Exclude(if="true") // never serialize the file object back out
     */
    private ?UploadedFile $imageFile = null;

    private ?string $imageName = null;

    // getters/setters, setUpdatedAt() in setImageFile() if you rely on Vich's date-based update hook
}

Client payload:

{ "name": "Widget", "imageFile": "/9j/4AAQSkZJRgABAQ..." }

Controller:

$product = $this->serializer->deserialize($request->getContent(), Product::class, 'json');
$em->persist($product);
$em->flush(); // Vich's listener moves the file and fills imageName

That single flow — JSON in, UploadedFile out, Vich moves it — is 90% of day-to-day usage.

Implementation Patterns

DTO-first, then map to the entity. Deserializing straight into a Doctrine entity couples your API contract to your persistence model and makes constructor-based JMS deserialization painful. Keep an UploadProductInput DTO with the vich_upload property, validate it, then copy fields onto the entity in a service. It also keeps the UploadedFile from leaking into serialized responses.

Output URLs via virtual properties, not the file property. The file property should be write-only (@Serializer\Exclude). Expose the public path with Vich's helper:

/**
 * @Serializer\VirtualProperty
 * @Serializer\SerializedName("imageUrl")
 * @Serializer\Groups({"read"})
 */
public function getImageUrl(): ?string
{
    return $this->imageName
        ? $this->uploaderHelper->asset($this, 'imageFile')
        : null;
}

Inject Vich\UploaderBundle\Templating\Helper\UploaderHelper (or vich_uploader.templating.helper.uploader_helper) — fully autowirable on Symfony 6 — and combine with an asset base URL. Never expose imageName as a filesystem path.

PATCH / partial updates. JMS only invokes the handler when the key is present, so a missing imageFile naturally means "don't touch the file." For explicit removal, add a removeImage boolean to the DTO and null out imageFile/imageName yourself — the bridge has no concept of deletion.

Collections of files. If you need array<vich_upload>, verify the handler accepts a list type; if not, wrap the array in a value object with a dedicated property per index, or register a custom handler (below). Multiple uploads in one request are the most common reason to extend the bundle.

Validation after deserialization. Attach @Assert\File(maxSize="5M", mimeTypes={"image/jpeg","image/png"}) to the DTO property and run the validator in the controller. Remember the base64 decode happens before validation, so the validator cannot protect you from a 200 MB payload — cap request size at the web server and add an early length check.

Extension point: your own handler. The bundle's handler is intentionally narrow. To support URLs ("imageFile": "https://cdn/..."), S3 pre-signed references, or a different encoding, implement JMS\Serializer\Handler\SubscribingHandlerInterface for UploadedFile, register it with jms_serializer.subscribing_handler, and give it a distinct type name (e.g. app_upload_url) so both handlers can coexist. This is also the cleanest escape hatch if you'd rather not depend on the package — the handler is small enough to vendor into your app, and it is unchanged in 3.3.0, so a copy taken today is current.

Storage abstraction is preserved. The handler only creates the UploadedFile; Vich's storage service (vich_uploader.storage.local, ...gaufrette, S3 adapters) still performs the move. That means S3/Gaufrette setups keep working unchanged, and tests can swap in a filesystem storage.

Gotchas and Tips

  • v3.3.0 is a compatibility bump, not a feature release. The only change is Symfony 6 dependency constraints; behaviour, type names, and config are identical to 3.2.x. Don't expect a fix for any known issue — if you were blocked on something other than installability, you still need the vendor-it-yourself route. Still pin the version in composer.json.
  • Verify the rest of the triangle on Symfony 6. The bundle now permits Symfony 6, but JMSSerializerBundle and VichUploaderBundle must also resolve to Symfony 6-compatible majors, and the handler registration depends on the JMS version you land on. Run composer why-not after install, then the debug:container --tag=jms_serializer.subscribing_handler check. Compatibility failures usually show up as a missing or silently unregistered handler, not a hard error.
  • Low maintenance signal. One dependency-only commit does not mean active maintenance. Budget for vendoring the handler, and treat any future Symfony 7 upgrade as unsupported until proven otherwise.
  • The test flag trap. UploadedFile created with $test = true returns from move() without moving anything. If uploads "succeed" but no file appears, inspect the object's isTest() — it's the single most common silent failure in bridges like this.
  • Spoofed names and MIME types. Base64 payloads have no trustworthy getClientOriginalName()/getClientMimeType(). Do not name files from the payload and do not trust its MIME. Force a namer (uuid, sha1, or a custom NamerInterface) and validate with @Assert\File(mimeTypes: ...), which uses finfo on the temp file.
  • Request size and memory. Base64 inflates payloads ~33%. Nginx client_max_body_size and PHP memory_limit bite before post_max_size does for JSON bodies. Reject oversized strings in a custom handler or a kernel request listener.
  • Direction matters. @Serializer\Exclude on the file property prevents JMS from trying to serialize a live UploadedFile (which produces garbage or throws). Use groups (read/write) rather than excluding entirely if you ever need to inspect the input DTO.
  • Attributes vs annotations. The bundle's README era targets annotations, and 3.3.0 changes nothing there. JMS 3.x and Vich 2.x support PHP 8 attributes, but mixing attribute-mapped entities with annotation-configured JMS metadata directories (config/serializer) is a frequent "type not applied" cause — pick one mapping style per class and verify with bin/console debug:config jms_serializer.
  • Constructors. JMS deserializes through the constructor when it can; an UploadedFile argument in a constructor will not be handled reliably. Use setters or @Serializer\Accessor for the file property.
  • Error handling. Handler failures surface as JMS\Serializer\Exception\RuntimeException (or a subclass). Catch it in the controller and convert to a 400 with a generic message — never echo the base64 content or filesystem paths back.
  • Doctrine update hooks. Vich only moves files on prePersist/preUpdate. If the entity is otherwise unchanged, the update may not fire; the standard fix is touching an updatedAt field inside setImageFile().
  • Debugging checklist: debug:container --tag=jms_serializer.subscribing_handler (is the handler there?), debug:config vich_uploader (is the mapping name right?), a profiler look at the serializer collector (did the type resolve?), and var_dump($uploadedFile->isValid()) before flush.
  • Testing. Build fixtures by decoding a small base64 string into a temp file, and assert both the entity state and the physical file after flush(). Set vich_uploader.mappings.<mapping>.uri_prefix to a test value so asset() assertions are deterministic. Nothing in 3.3.0 changes this, so existing fixtures keep passing after the bump.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle